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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/docs/content/docs/plugins/oidc-provider.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: OIDC Provider
  3 | description: Open ID Connect plugin for Better Auth that allows you to have your own OIDC provider.
  4 | ---
  5 | 
  6 | The **OIDC Provider Plugin** enables you to build and manage your own OpenID Connect (OIDC) provider, granting full control over user authentication without relying on third-party services like Okta or Azure AD. It also allows other services to authenticate users through your OIDC provider.
  7 | 
  8 | **Key Features**:
  9 | 
 10 | - **Client Registration**: Register clients to authenticate with your OIDC provider.
 11 | - **Dynamic Client Registration**: Allow clients to register dynamically.
 12 | - **Trusted Clients**: Configure hard-coded trusted clients with optional consent bypass.
 13 | - **Authorization Code Flow**: Support the Authorization Code Flow.
 14 | - **Public Clients**: Support public clients for SPA, mobile apps, CLI tools, etc.
 15 | - **JWKS Endpoint**: Publish a JWKS endpoint to allow clients to verify tokens. (Not fully implemented)
 16 | - **Refresh Tokens**: Issue refresh tokens and handle access token renewal using the `refresh_token` grant.
 17 | - **OAuth Consent**: Implement OAuth consent screens for user authorization, with an option to bypass consent for trusted applications.
 18 | - **UserInfo Endpoint**: Provide a UserInfo endpoint for clients to retrieve user details.
 19 | 
 20 | <Callout type="warn">
 21 | This plugin is in active development and may not be suitable for production use. Please report any issues or bugs on [GitHub](https://github.com/better-auth/better-auth).
 22 | </Callout>
 23 | 
 24 | ## Installation
 25 | 
 26 | <Steps>
 27 |     <Step>
 28 |         ### Mount the Plugin
 29 | 
 30 |         Add the OIDC plugin to your auth config. See [OIDC Configuration](#oidc-configuration) on how to configure the plugin.
 31 | 
 32 |         ```ts title="auth.ts"
 33 |         import { betterAuth } from "better-auth";
 34 |         import { oidcProvider } from "better-auth/plugins";
 35 | 
 36 |         const auth = betterAuth({
 37 |             plugins: [oidcProvider({
 38 |                 loginPage: "/sign-in", // path to the login page
 39 |                 // ...other options
 40 |             })]
 41 |         })
 42 |         ```
 43 |     </Step>
 44 | 
 45 |     <Step>
 46 |         ### Migrate the Database
 47 | 
 48 |         Run the migration or generate the schema to add the necessary fields and tables to the database.
 49 | 
 50 |         <Tabs items={["migrate", "generate"]}>
 51 |             <Tab value="migrate">
 52 |             ```bash
 53 |             npx @better-auth/cli migrate
 54 |             ```
 55 |             </Tab>
 56 |             <Tab value="generate">
 57 |             ```bash
 58 |             npx @better-auth/cli generate
 59 |             ```
 60 |             </Tab>
 61 |         </Tabs>
 62 |         See the [Schema](#schema) section to add the fields manually.
 63 |     </Step>
 64 | 
 65 |     <Step>
 66 |         ### Add the Client Plugin
 67 | 
 68 |         Add the OIDC client plugin to your auth client config.
 69 | 
 70 |         ```ts
 71 |         import { createAuthClient } from "better-auth/client";
 72 |         import { oidcClient } from "better-auth/client/plugins"
 73 |         const authClient = createAuthClient({
 74 |             plugins: [oidcClient({
 75 |                 // Your OIDC configuration
 76 |             })]
 77 |         })
 78 |         ```
 79 |     </Step>
 80 | </Steps>
 81 | 
 82 | ## Usage
 83 | 
 84 | Once installed, you can utilize the OIDC Provider to manage authentication flows within your application.
 85 | 
 86 | ### Register a New Client
 87 | 
 88 | To register a new OIDC client, use the `oauth2.register` method.
 89 | 
 90 | 
 91 | #### Simple Example
 92 | 
 93 | ```ts
 94 | const application = await client.oauth2.register({
 95 |     client_name: "My Client",
 96 |     redirect_uris: ["https://client.example.com/callback"],
 97 | });
 98 | ```
 99 | 
100 | 
101 | #### Full Method
102 | 
103 | 
104 | <APIMethod path="/oauth2/register" method="POST">
105 | ```ts
106 | type registerOAuthApplication = {
107 |     /**
108 |      * A list of redirect URIs. 
109 |      */
110 |     redirect_uris: string[] = ["https://client.example.com/callback"]
111 |     /**
112 |      * The authentication method for the token endpoint. 
113 |      */
114 |     token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post" = "client_secret_basic"
115 |     /**
116 |      * The grant types supported by the application. 
117 |      */
118 |     grant_types?: ("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[] = ["authorization_code"]
119 |     /**
120 |      * The response types supported by the application. 
121 |      */
122 |     response_types?: ("code" | "token")[] = ["code"]
123 |     /**
124 |      * The name of the application. 
125 |      */
126 |     client_name?: string = "My App"
127 |     /**
128 |      * The URI of the application. 
129 |      */
130 |     client_uri?: string = "https://client.example.com"
131 |     /**
132 |      * The URI of the application logo. 
133 |      */
134 |     logo_uri?: string = "https://client.example.com/logo.png"
135 |     /**
136 |      * The scopes supported by the application. Separated by spaces. 
137 |      */
138 |     scope?: string = "profile email"
139 |     /**
140 |      * The contact information for the application. 
141 |      */
142 |     contacts?: string[] = ["[email protected]"]
143 |     /**
144 |      * The URI of the application terms of service. 
145 |      */
146 |     tos_uri?: string = "https://client.example.com/tos"
147 |     /**
148 |      * The URI of the application privacy policy. 
149 |      */
150 |     policy_uri?: string = "https://client.example.com/policy"
151 |     /**
152 |      * The URI of the application JWKS. 
153 |      */
154 |     jwks_uri?: string = "https://client.example.com/jwks"
155 |     /**
156 |      * The JWKS of the application. 
157 |      */
158 |     jwks?: Record<string, any> = {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]}
159 |     /**
160 |      * The metadata of the application. 
161 |      */
162 |     metadata?: Record<string, any> = {"key": "value"}
163 |     /**
164 |      * The software ID of the application. 
165 |      */
166 |     software_id?: string = "my-software"
167 |     /**
168 |      * The software version of the application. 
169 |      */
170 |     software_version?: string = "1.0.0"
171 |     /**
172 |      * The software statement of the application. 
173 |      */
174 |     software_statement?: string
175 | }
176 | ```
177 | </APIMethod>
178 | 
179 | <Callout>
180 | This endpoint supports [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591) compliant client registration.
181 | </Callout>
182 | 
183 | Once the application is created, you will receive a `client_id` and `client_secret` that you can display to the user.
184 | 
185 | ### Trusted Clients
186 | 
187 | For first-party applications and internal services, you can configure trusted clients directly in your OIDC provider configuration. Trusted clients bypass database lookups for better performance and can optionally skip consent screens for improved user experience.
188 | 
189 | ```ts title="auth.ts"
190 | import { betterAuth } from "better-auth";
191 | import { oidcProvider } from "better-auth/plugins";
192 | 
193 | const auth = betterAuth({
194 |     plugins: [
195 |       oidcProvider({
196 |         loginPage: "/sign-in",
197 |         trustedClients: [
198 |             {
199 |                 clientId: "internal-dashboard",
200 |                 clientSecret: "secure-secret-here",
201 |                 name: "Internal Dashboard",
202 |                 type: "web",
203 |                 redirectURLs: ["https://dashboard.company.com/auth/callback"],
204 |                 disabled: false,
205 |                 skipConsent: true, // Skip consent for this trusted client
206 |                 metadata: { internal: true }
207 |             },
208 |             {
209 |                 clientId: "mobile-app",
210 |                 clientSecret: "mobile-secret", 
211 |                 name: "Company Mobile App",
212 |                 type: "native",
213 |                 redirectURLs: ["com.company.app://auth"],
214 |                 disabled: false,
215 |                 skipConsent: false, // Still require consent if needed
216 |                 metadata: {}
217 |             }
218 |         ]
219 |     })]
220 | })
221 | ```
222 | 
223 | ### UserInfo Endpoint
224 | 
225 | The OIDC Provider includes a UserInfo endpoint that allows clients to retrieve information about the authenticated user. This endpoint is available at `/oauth2/userinfo` and requires a valid access token.
226 | 
227 | <Endpoint path="/oauth2/userinfo" method="GET" />
228 | 
229 | ```ts title="client-app.ts"
230 | // Example of how a client would use the UserInfo endpoint
231 | const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
232 |   headers: {
233 |     'Authorization': 'Bearer ACCESS_TOKEN'
234 |   }
235 | });
236 | 
237 | const userInfo = await response.json();
238 | // userInfo contains user details based on the scopes granted
239 | ```
240 | 
241 | The UserInfo endpoint returns different claims based on the scopes that were granted during authorization:
242 | 
243 | - With `openid` scope: Returns the user's ID (`sub` claim)
244 | - With `profile` scope: Returns name, picture, given_name, family_name
245 | - With `email` scope: Returns email and email_verified
246 | 
247 | The `getAdditionalUserInfoClaim` function receives the user object, requested scopes array, and the client, allowing you to conditionally include claims based on the scopes granted during authorization. These additional claims will be included in both the UserInfo endpoint response and the ID token.
248 | 
249 | ### Consent Screen
250 | 
251 | When a user is redirected to the OIDC provider for authentication, they may be prompted to authorize the application to access their data. This is known as the consent screen. By default, Better Auth will display a sample consent screen. You can customize the consent screen by providing a `consentPage` option during initialization.
252 | 
253 | **Note**: Trusted clients with `skipConsent: true` will bypass the consent screen entirely, providing a seamless experience for first-party applications.
254 | 
255 | ```ts title="auth.ts"
256 | import { betterAuth } from "better-auth";
257 | 
258 | export const auth = betterAuth({
259 |     plugins: [oidcProvider({
260 |         consentPage: "/path/to/consent/page"
261 |     })]
262 | })
263 | ```
264 | 
265 | The plugin will redirect the user to the specified path with `consent_code`, `client_id` and `scope` query parameters. You can use this information to display a custom consent screen. Once the user consents, you can call `oauth2.consent` to complete the authorization.
266 | 
267 | <Endpoint path="/oauth2/consent" method="POST" />
268 | 
269 | The consent endpoint supports two methods for passing the consent code:
270 | 
271 | **Method 1: URL Parameter**
272 | ```ts title="consent-page.ts"
273 | // Get the consent code from the URL
274 | const params = new URLSearchParams(window.location.search);
275 | 
276 | // Submit consent with the code in the request body
277 | const consentCode = params.get('consent_code');
278 | if (!consentCode) {
279 | 	throw new Error('Consent code not found in URL parameters');
280 | }
281 | 
282 | const res = await client.oauth2.consent({
283 | 	accept: true, // or false to deny
284 | 	consent_code: consentCode,
285 | });
286 | ```
287 | 
288 | **Method 2: Cookie-Based**
289 | ```ts title="consent-page.ts"
290 | // The consent code is automatically stored in a signed cookie
291 | // Just submit the consent decision
292 | const res = await client.oauth2.consent({
293 | 	accept: true, // or false to deny
294 | 	// consent_code not needed when using cookie-based flow
295 | });
296 | ```
297 | 
298 | Both methods are fully supported. The URL parameter method works well with mobile apps and third-party contexts, while the cookie-based method provides a simpler implementation for web applications.
299 | 
300 | ### Handling Login
301 | 
302 | When a user is redirected to the OIDC provider for authentication, if they are not already logged in, they will be redirected to the login page. You can customize the login page by providing a `loginPage` option during initialization.
303 | 
304 | ```ts title="auth.ts"
305 | import { betterAuth } from "better-auth";
306 | 
307 | export const auth = betterAuth({
308 |     plugins: [oidcProvider({
309 |         loginPage: "/sign-in"
310 |     })]
311 | })
312 | ```
313 | 
314 | You don't need to handle anything from your side; when a new session is created, the plugin will handle continuing the authorization flow.
315 | 
316 | ## Configuration
317 | 
318 | ### OIDC Metadata
319 | 
320 | Customize the OIDC metadata by providing a configuration object during initialization.
321 | 
322 | ```ts title="auth.ts"
323 | import { betterAuth } from "better-auth";
324 | import { oidcProvider } from "better-auth/plugins";
325 | 
326 | export const auth = betterAuth({
327 |     plugins: [oidcProvider({
328 |         metadata: {
329 |             issuer: "https://your-domain.com",
330 |             authorization_endpoint: "/custom/oauth2/authorize",
331 |             token_endpoint: "/custom/oauth2/token",
332 |             // ...other custom metadata
333 |         }
334 |     })]
335 | })
336 | ```
337 | 
338 | ### JWKS Endpoint
339 | 
340 | The OIDC Provider plugin can integrate with the JWT plugin to provide asymmetric key signing for ID tokens verifiable at a JWKS endpoint.
341 | 
342 | To make your plugin OIDC compliant, you **MUST** disable the `/token` endpoint, the OAuth equivalent is located at `/oauth2/token` instead.
343 | 
344 | ```ts title="auth.ts"
345 | import { betterAuth } from "better-auth";
346 | import { oidcProvider } from "better-auth/plugins";
347 | import { jwt } from "better-auth/plugins";
348 | 
349 | export const auth = betterAuth({
350 |     disabledPaths: [
351 |         "/token",
352 |     ],
353 |     plugins: [
354 |         jwt(), // Make sure to add the JWT plugin
355 |         oidcProvider({
356 |             useJWTPlugin: true, // Enable JWT plugin integration
357 |             loginPage: "/sign-in",
358 |             // ... other options
359 |         })
360 |     ]
361 | })
362 | ```
363 | 
364 | <Callout type="info">
365 | When `useJWTPlugin: false` (default), ID tokens are signed with the application secret.
366 | </Callout>
367 | 
368 | ### Dynamic Client Registration
369 | 
370 | If you want to allow clients to register dynamically, you can enable this feature by setting the `allowDynamicClientRegistration` option to `true`.
371 | 
372 | ```ts title="auth.ts"
373 | const auth = betterAuth({
374 |     plugins: [oidcProvider({
375 |         allowDynamicClientRegistration: true,
376 |     })]
377 | })
378 | ```
379 | 
380 | This will allow clients to register using the `/register` endpoint to be publicly available.
381 | 
382 | ## Schema
383 | 
384 | The OIDC Provider plugin adds the following tables to the database:
385 | 
386 | ### OAuth Application
387 | 
388 | Table Name: `oauthApplication`
389 | 
390 | <DatabaseTable
391 |   fields={[
392 |    {
393 |       name: "id",
394 |       type: "string",
395 |       description: "Database ID of the OAuth client",
396 |       isPrimaryKey: true
397 |    },
398 |     { 
399 |       name: "clientId", 
400 |       type: "string", 
401 |       description: "Unique identifier for each OAuth client",
402 |       isPrimaryKey: true
403 |     },
404 |     { 
405 |       name: "clientSecret", 
406 |       type: "string", 
407 |       description: "Secret key for the OAuth client. Optional for public clients using PKCE.",
408 |       isOptional: true
409 |     },
410 |     { 
411 |       name: "name", 
412 |       type: "string", 
413 |       description: "Name of the OAuth client",
414 |       isRequired: true
415 |     },
416 |     { 
417 |       name: "redirectURLs", 
418 |       type: "string", 
419 |       description: "Comma-separated list of redirect URLs",
420 |       isRequired: true
421 |     },
422 |     { 
423 |       name: "metadata", 
424 |       type: "string",
425 |       description: "Additional metadata for the OAuth client",
426 |       isOptional: true
427 |     },
428 |     { 
429 |       name: "type", 
430 |       type: "string",
431 |       description: "Type of OAuth client (e.g., web, mobile)",
432 |       isRequired: true
433 |     },
434 |     { 
435 |       name: "disabled", 
436 |       type: "boolean", 
437 |       description: "Indicates if the client is disabled",
438 |       isRequired: true
439 |     },
440 |     { 
441 |       name: "userId", 
442 |       type: "string", 
443 |       description: "ID of the user who owns the client. (optional)",
444 |       isOptional: true,
445 |       references: { model: "user", field: "id" }
446 |     },
447 |     { 
448 |       name: "createdAt", 
449 |       type: "Date", 
450 |       description: "Timestamp of when the OAuth client was created" 
451 |     },
452 |    {
453 |       name: "updatedAt",
454 |       type: "Date",
455 |       description: "Timestamp of when the OAuth client was last updated"
456 |    }
457 |   ]}
458 | />
459 | 
460 | ### OAuth Access Token
461 | 
462 | Table Name: `oauthAccessToken`
463 | 
464 | <DatabaseTable
465 |   fields={[
466 |     {
467 |       name: "id",
468 |       type: "string",
469 |       description: "Database ID of the access token",
470 |       isPrimaryKey: true
471 |    },
472 |     { 
473 |       name: "accessToken", 
474 |       type: "string", 
475 |       description: "Access token issued to the client",
476 |     },
477 |     { 
478 |       name: "refreshToken", 
479 |       type: "string", 
480 |       description: "Refresh token issued to the client",
481 |       isRequired: true
482 |     },
483 |     { 
484 |       name: "accessTokenExpiresAt", 
485 |       type: "Date", 
486 |       description: "Expiration date of the access token",
487 |       isRequired: true
488 |     },
489 |     { 
490 |       name: "refreshTokenExpiresAt", 
491 |       type: "Date", 
492 |       description: "Expiration date of the refresh token",
493 |       isRequired: true
494 |     },
495 |     { 
496 |       name: "clientId", 
497 |       type: "string", 
498 |       description: "ID of the OAuth client",
499 |       isForeignKey: true,
500 |       references: { model: "oauthApplication", field: "clientId" }
501 |     },
502 |     { 
503 |       name: "userId", 
504 |       type: "string", 
505 |       description: "ID of the user associated with the token",
506 |       isForeignKey: true,
507 |       references: { model: "user", field: "id" }
508 |     },
509 |     { 
510 |       name: "scopes", 
511 |       type: "string", 
512 |       description: "Comma-separated list of scopes granted",
513 |       isRequired: true
514 |     },
515 |     { 
516 |       name: "createdAt", 
517 |       type: "Date", 
518 |       description: "Timestamp of when the access token was created" 
519 |     },
520 |     {
521 |       name: "updatedAt",
522 |       type: "Date",
523 |       description: "Timestamp of when the access token was last updated"
524 |     }
525 |   ]}
526 | />
527 | 
528 | ### OAuth Consent
529 | 
530 | Table Name: `oauthConsent`
531 | 
532 | <DatabaseTable
533 |   fields={[
534 |     { 
535 |       name: "id", 
536 |       type: "string", 
537 |       description: "Database ID of the consent",
538 |       isPrimaryKey: true
539 |     },
540 |     { 
541 |       name: "userId", 
542 |       type: "string", 
543 |       description: "ID of the user who gave consent",
544 |       isForeignKey: true,
545 |       references: { model: "user", field: "id" }
546 |     },
547 |     { 
548 |       name: "clientId", 
549 |       type: "string", 
550 |       description: "ID of the OAuth client",
551 |       isForeignKey: true,
552 |       references: { model: "oauthApplication", field: "clientId" }
553 |     },
554 |     { 
555 |       name: "scopes", 
556 |       type: "string", 
557 |       description: "Comma-separated list of scopes consented to",
558 |       isRequired: true
559 |     },
560 |     { 
561 |       name: "consentGiven", 
562 |       type: "boolean", 
563 |       description: "Indicates if consent was given",
564 |       isRequired: true
565 |     },
566 |     { 
567 |       name: "createdAt", 
568 |       type: "Date", 
569 |       description: "Timestamp of when the consent was given" 
570 |     },
571 |     {
572 |       name: "updatedAt",
573 |       type: "Date",
574 |       description: "Timestamp of when the consent was last updated"
575 |     }
576 |   ]}
577 | />
578 | 
579 | ## Options
580 | 
581 | **allowDynamicClientRegistration**: `boolean` - Enable or disable dynamic client registration.
582 | 
583 | **metadata**: `OIDCMetadata` - Customize the OIDC provider metadata.
584 | 
585 | **loginPage**: `string` - Path to the custom login page.
586 | 
587 | **consentPage**: `string` - Path to the custom consent page.
588 | 
589 | **trustedClients**: `(Client & { skipConsent?: boolean })[]` - Array of trusted clients that are configured directly in the provider options. These clients bypass database lookups and can optionally skip consent screens.
590 | 
591 | **getAdditionalUserInfoClaim**: `(user: User, scopes: string[], client: Client) => Record<string, any>` - Function to get additional user info claims.
592 | 
593 | **useJWTPlugin**: `boolean` - When `true`, ID tokens are signed using the JWT plugin's asymmetric keys. When `false` (default), ID tokens are signed with HMAC-SHA256 using the application secret.
594 | 
595 | **schema**: `AuthPluginSchema` - Customize the OIDC provider schema.
596 | 
```

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

```typescript
  1 | import type { DBFieldAttribute } from "@better-auth/core/db";
  2 | import type { User, Session } from "../../types";
  3 | import type { AccessControl, Role } from "../access";
  4 | import type {
  5 | 	Invitation,
  6 | 	Member,
  7 | 	Organization,
  8 | 	OrganizationRole,
  9 | 	Team,
 10 | 	TeamMember,
 11 | } from "./schema";
 12 | import type { AuthContext } from "@better-auth/core";
 13 | 
 14 | export interface OrganizationOptions {
 15 | 	/**
 16 | 	 * Configure whether new users are able to create new organizations.
 17 | 	 * You can also pass a function that returns a boolean.
 18 | 	 *
 19 | 	 * 	@example
 20 | 	 * ```ts
 21 | 	 * allowUserToCreateOrganization: async (user) => {
 22 | 	 * 		const plan = await getUserPlan(user);
 23 | 	 *      return plan.name === "pro";
 24 | 	 * }
 25 | 	 * ```
 26 | 	 * @default true
 27 | 	 */
 28 | 	allowUserToCreateOrganization?:
 29 | 		| boolean
 30 | 		| ((user: User & Record<string, any>) => Promise<boolean> | boolean);
 31 | 	/**
 32 | 	 * The maximum number of organizations a user can create.
 33 | 	 *
 34 | 	 * You can also pass a function that returns a boolean
 35 | 	 */
 36 | 	organizationLimit?: number | ((user: User) => Promise<boolean> | boolean);
 37 | 	/**
 38 | 	 * The role that is assigned to the creator of the
 39 | 	 * organization.
 40 | 	 *
 41 | 	 * @default "owner"
 42 | 	 */
 43 | 	creatorRole?: string;
 44 | 	/**
 45 | 	 * The maximum number of members allowed in an organization.
 46 | 	 *
 47 | 	 * @default 100
 48 | 	 */
 49 | 	membershipLimit?: number;
 50 | 	/**
 51 | 	 * Configure the roles and permissions for the
 52 | 	 * organization plugin.
 53 | 	 */
 54 | 	ac?: AccessControl;
 55 | 	/**
 56 | 	 * Custom permissions for roles.
 57 | 	 */
 58 | 	roles?: {
 59 | 		[key in string]?: Role<any>;
 60 | 	};
 61 | 	/**
 62 | 	 * Dynamic access control for the organization plugin.
 63 | 	 */
 64 | 	dynamicAccessControl?: {
 65 | 		/**
 66 | 		 * Whether to enable dynamic access control for the organization plugin.
 67 | 		 *
 68 | 		 * @default false
 69 | 		 */
 70 | 		enabled?: boolean;
 71 | 		/**
 72 | 		 * The maximum number of roles that can be created for an organization.
 73 | 		 *
 74 | 		 * @default Infinite
 75 | 		 */
 76 | 		maximumRolesPerOrganization?:
 77 | 			| number
 78 | 			| ((organizationId: string) => Promise<number> | number);
 79 | 	};
 80 | 	/**
 81 | 	 * Support for team.
 82 | 	 */
 83 | 	teams?: {
 84 | 		/**
 85 | 		 * Enable team features.
 86 | 		 */
 87 | 		enabled: boolean;
 88 | 		/**
 89 | 		 * Default team configuration
 90 | 		 */
 91 | 		defaultTeam?: {
 92 | 			/**
 93 | 			 * Enable creating a default team when an organization is created
 94 | 			 *
 95 | 			 * @default true
 96 | 			 */
 97 | 			enabled: boolean;
 98 | 			/**
 99 | 			 * Pass a custom default team creator function
100 | 			 */
101 | 			customCreateDefaultTeam?: (
102 | 				organization: Organization & Record<string, any>,
103 | 				request?: Request,
104 | 			) => Promise<Team & Record<string, any>>;
105 | 		};
106 | 		/**
107 | 		 * Maximum number of teams an organization can have.
108 | 		 *
109 | 		 * You can pass a number or a function that returns a number
110 | 		 *
111 | 		 * @default "unlimited"
112 | 		 *
113 | 		 * @param organization
114 | 		 * @param request
115 | 		 * @returns
116 | 		 */
117 | 		maximumTeams?:
118 | 			| ((
119 | 					data: {
120 | 						organizationId: string;
121 | 						session: {
122 | 							user: User;
123 | 							session: Session;
124 | 						} | null;
125 | 					},
126 | 					request?: Request,
127 | 			  ) => number | Promise<number>)
128 | 			| number;
129 | 
130 | 		/**
131 | 		 * The maximum number of members per team.
132 | 		 *
133 | 		 * if `undefined`, there is no limit.
134 | 		 *
135 | 		 * @default undefined
136 | 		 */
137 | 		maximumMembersPerTeam?:
138 | 			| number
139 | 			| ((data: {
140 | 					teamId: string;
141 | 					session: { user: User; session: Session };
142 | 					organizationId: string;
143 | 			  }) => Promise<number> | number)
144 | 			| undefined;
145 | 		/**
146 | 		 * By default, if an organization does only have one team, they'll not be able to remove it.
147 | 		 *
148 | 		 * You can disable this behavior by setting this to `false.
149 | 		 *
150 | 		 * @default false
151 | 		 */
152 | 		allowRemovingAllTeams?: boolean;
153 | 	};
154 | 	/**
155 | 	 * The expiration time for the invitation link.
156 | 	 *
157 | 	 * @default 48 hours
158 | 	 */
159 | 	invitationExpiresIn?: number;
160 | 	/**
161 | 	 * The maximum invitation a user can send.
162 | 	 *
163 | 	 * @default 100
164 | 	 */
165 | 	invitationLimit?:
166 | 		| number
167 | 		| ((
168 | 				data: {
169 | 					user: User;
170 | 					organization: Organization;
171 | 					member: Member;
172 | 				},
173 | 				ctx: AuthContext,
174 | 		  ) => Promise<number> | number);
175 | 	/**
176 | 	 * Cancel pending invitations on re-invite.
177 | 	 *
178 | 	 * @default false
179 | 	 */
180 | 	cancelPendingInvitationsOnReInvite?: boolean;
181 | 	/**
182 | 	 * Require email verification on accepting or rejecting an invitation
183 | 	 *
184 | 	 * @default false
185 | 	 */
186 | 	requireEmailVerificationOnInvitation?: boolean;
187 | 	/**
188 | 	 * Send an email with the
189 | 	 * invitation link to the user.
190 | 	 *
191 | 	 * Note: Better Auth doesn't
192 | 	 * generate invitation URLs.
193 | 	 * You'll need to construct the
194 | 	 * URL using the invitation ID
195 | 	 * and pass it to the
196 | 	 * acceptInvitation endpoint for
197 | 	 * the user to accept the
198 | 	 * invitation.
199 | 	 *
200 | 	 * @example
201 | 	 * ```ts
202 | 	 * sendInvitationEmail: async (data) => {
203 | 	 * 	const url = `https://yourapp.com/organization/
204 | 	 * accept-invitation?id=${data.id}`;
205 | 	 * 	await sendEmail(data.email, "Invitation to join
206 | 	 * organization", `Click the link to join the
207 | 	 * organization: ${url}`);
208 | 	 * }
209 | 	 * ```
210 | 	 */
211 | 	sendInvitationEmail?: (
212 | 		data: {
213 | 			/**
214 | 			 * the invitation id
215 | 			 */
216 | 			id: string;
217 | 			/**
218 | 			 * the role of the user
219 | 			 */
220 | 			role: string;
221 | 			/**
222 | 			 * the email of the user
223 | 			 */
224 | 			email: string;
225 | 			/**
226 | 			 * the organization the user is invited to join
227 | 			 */
228 | 			organization: Organization;
229 | 			/**
230 | 			 * the invitation object
231 | 			 */
232 | 			invitation: Invitation;
233 | 			/**
234 | 			 * the member who is inviting the user
235 | 			 */
236 | 			inviter: Member & {
237 | 				user: User;
238 | 			};
239 | 		},
240 | 		/**
241 | 		 * The request object
242 | 		 */
243 | 		request?: Request,
244 | 	) => Promise<void>;
245 | 	/**
246 | 	 * The schema for the organization plugin.
247 | 	 */
248 | 	schema?: {
249 | 		session?: {
250 | 			fields?: {
251 | 				activeOrganizationId?: string;
252 | 				activeTeamId?: string;
253 | 			};
254 | 		};
255 | 		organization?: {
256 | 			modelName?: string;
257 | 			fields?: {
258 | 				[key in keyof Omit<Organization, "id">]?: string;
259 | 			};
260 | 			additionalFields?: {
261 | 				[key in string]: DBFieldAttribute;
262 | 			};
263 | 		};
264 | 		member?: {
265 | 			modelName?: string;
266 | 			fields?: {
267 | 				[key in keyof Omit<Member, "id">]?: string;
268 | 			};
269 | 			additionalFields?: {
270 | 				[key in string]: DBFieldAttribute;
271 | 			};
272 | 		};
273 | 		invitation?: {
274 | 			modelName?: string;
275 | 			fields?: {
276 | 				[key in keyof Omit<Invitation, "id">]?: string;
277 | 			};
278 | 			additionalFields?: {
279 | 				[key in string]: DBFieldAttribute;
280 | 			};
281 | 		};
282 | 		team?: {
283 | 			modelName?: string;
284 | 			fields?: {
285 | 				[key in keyof Omit<Team, "id">]?: string;
286 | 			};
287 | 			additionalFields?: {
288 | 				[key in string]: DBFieldAttribute;
289 | 			};
290 | 		};
291 | 		teamMember?: {
292 | 			modelName?: string;
293 | 			fields?: {
294 | 				[key in keyof Omit<TeamMember, "id">]?: string;
295 | 			};
296 | 		};
297 | 		organizationRole?: {
298 | 			modelName?: string;
299 | 			fields?: {
300 | 				[key in keyof Omit<OrganizationRole, "id">]?: string;
301 | 			};
302 | 			additionalFields?: {
303 | 				[key in string]: DBFieldAttribute;
304 | 			};
305 | 		};
306 | 	};
307 | 	/**
308 | 	 * Disable organization deletion
309 | 	 *
310 | 	 * @default false
311 | 	 */
312 | 	disableOrganizationDeletion?: boolean;
313 | 	/**
314 | 	 * Configure how organization deletion is handled
315 | 	 *
316 | 	 * @deprecated Use `organizationHooks` instead
317 | 	 */
318 | 	organizationDeletion?: {
319 | 		/**
320 | 		 * disable deleting organization
321 | 		 *
322 | 		 * @deprecated Use `disableOrganizationDeletion` instead
323 | 		 */
324 | 		disabled?: boolean;
325 | 		/**
326 | 		 * A callback that runs before the organization is
327 | 		 * deleted
328 | 		 *
329 | 		 * @deprecated Use `organizationHooks` instead
330 | 		 * @param data - organization and user object
331 | 		 * @param request - the request object
332 | 		 * @returns
333 | 		 */
334 | 		beforeDelete?: (
335 | 			data: {
336 | 				organization: Organization;
337 | 				user: User;
338 | 			},
339 | 			request?: Request,
340 | 		) => Promise<void>;
341 | 		/**
342 | 		 * A callback that runs after the organization is
343 | 		 * deleted
344 | 		 *
345 | 		 * @deprecated Use `organizationHooks` instead
346 | 		 * @param data - organization and user object
347 | 		 * @param request - the request object
348 | 		 * @returns
349 | 		 */
350 | 		afterDelete?: (
351 | 			data: {
352 | 				organization: Organization;
353 | 				user: User;
354 | 			},
355 | 			request?: Request,
356 | 		) => Promise<void>;
357 | 	};
358 | 	/**
359 | 	 * @deprecated Use `organizationHooks` instead
360 | 	 */
361 | 	organizationCreation?: {
362 | 		disabled?: boolean;
363 | 		beforeCreate?: (
364 | 			data: {
365 | 				organization: Omit<Organization, "id"> & Record<string, any>;
366 | 				user: User & Record<string, any>;
367 | 			},
368 | 			request?: Request,
369 | 		) => Promise<void | {
370 | 			data: Record<string, any>;
371 | 		}>;
372 | 		afterCreate?: (
373 | 			data: {
374 | 				organization: Organization & Record<string, any>;
375 | 				member: Member & Record<string, any>;
376 | 				user: User & Record<string, any>;
377 | 			},
378 | 			request?: Request,
379 | 		) => Promise<void>;
380 | 	};
381 | 	/**
382 | 	 * Hooks for organization
383 | 	 */
384 | 	organizationHooks?: {
385 | 		/**
386 | 		 * A callback that runs before the organization is created
387 | 		 *
388 | 		 * You can return a `data` object to override the default data.
389 | 		 *
390 | 		 * @example
391 | 		 * ```ts
392 | 		 * beforeCreateOrganization: async (data) => {
393 | 		 * 	return {
394 | 		 * 		data: {
395 | 		 * 			...data.organization,
396 | 		 * 		},
397 | 		 * 	};
398 | 		 * }
399 | 		 * ```
400 | 		 *
401 | 		 * You can also throw `new APIError` to stop the organization creation.
402 | 		 *
403 | 		 * @example
404 | 		 * ```ts
405 | 		 * beforeCreateOrganization: async (data) => {
406 | 		 * 	throw new APIError("BAD_REQUEST", {
407 | 		 * 		message: "Organization creation is disabled",
408 | 		 * 	});
409 | 		 * }
410 | 		 */
411 | 		beforeCreateOrganization?: (data: {
412 | 			organization: {
413 | 				name?: string;
414 | 				slug?: string;
415 | 				logo?: string;
416 | 				metadata?: Record<string, any>;
417 | 				[key: string]: any;
418 | 			};
419 | 			user: User & Record<string, any>;
420 | 		}) => Promise<void | {
421 | 			data: Record<string, any>;
422 | 		}>;
423 | 		/**
424 | 		 * A callback that runs after the organization is created
425 | 		 */
426 | 		afterCreateOrganization?: (data: {
427 | 			organization: Organization & Record<string, any>;
428 | 			member: Member & Record<string, any>;
429 | 			user: User & Record<string, any>;
430 | 		}) => Promise<void>;
431 | 		/**
432 | 		 * A callback that runs before the organization is updated
433 | 		 *
434 | 		 * You can return a `data` object to override the default data.
435 | 		 *
436 | 		 * @example
437 | 		 * ```ts
438 | 		 * beforeUpdateOrganization: async (data) => {
439 | 		 * 	return { data: { ...data.organization } };
440 | 		 * }
441 | 		 */
442 | 		beforeUpdateOrganization?: (data: {
443 | 			organization: {
444 | 				name?: string;
445 | 				slug?: string;
446 | 				logo?: string;
447 | 				metadata?: Record<string, any>;
448 | 				[key: string]: any;
449 | 			};
450 | 			user: User & Record<string, any>;
451 | 			member: Member & Record<string, any>;
452 | 		}) => Promise<void | {
453 | 			data: {
454 | 				name?: string;
455 | 				slug?: string;
456 | 				logo?: string;
457 | 				metadata?: Record<string, any>;
458 | 				[key: string]: any;
459 | 			};
460 | 		}>;
461 | 		/**
462 | 		 * A callback that runs after the organization is updated
463 | 		 *
464 | 		 * @example
465 | 		 * ```ts
466 | 		 * afterUpdateOrganization: async (data) => {
467 | 		 * 	console.log(data.organization);
468 | 		 * }
469 | 		 * ```
470 | 		 */
471 | 		afterUpdateOrganization?: (data: {
472 | 			/**
473 | 			 * Updated organization object
474 | 			 *
475 | 			 * This could be `null` if an adapter doesn't return updated organization.
476 | 			 */
477 | 			organization: (Organization & Record<string, any>) | null;
478 | 			user: User & Record<string, any>;
479 | 			member: Member & Record<string, any>;
480 | 		}) => Promise<void>;
481 | 		/**
482 | 		 * A callback that runs before the organization is deleted
483 | 		 */
484 | 		beforeDeleteOrganization?: (data: {
485 | 			organization: Organization & Record<string, any>;
486 | 			user: User & Record<string, any>;
487 | 		}) => Promise<void>;
488 | 		/**
489 | 		 * A callback that runs after the organization is deleted
490 | 		 */
491 | 		afterDeleteOrganization?: (data: {
492 | 			organization: Organization & Record<string, any>;
493 | 			user: User & Record<string, any>;
494 | 		}) => Promise<void>;
495 | 		/**
496 | 		 * Member hooks
497 | 		 */
498 | 
499 | 		/**
500 | 		 * A callback that runs before a member is added to an organization
501 | 		 *
502 | 		 * You can return a `data` object to override the default data.
503 | 		 *
504 | 		 * @example
505 | 		 * ```ts
506 | 		 * beforeAddMember: async (data) => {
507 | 		 * 	return {
508 | 		 * 		data: {
509 | 		 * 			...data.member,
510 | 		 * 			role: "custom-role"
511 | 		 * 		}
512 | 		 * 	};
513 | 		 * }
514 | 		 * ```
515 | 		 */
516 | 		beforeAddMember?: (data: {
517 | 			member: {
518 | 				userId: string;
519 | 				organizationId: string;
520 | 				role: string;
521 | 				[key: string]: any;
522 | 			};
523 | 			user: User & Record<string, any>;
524 | 			organization: Organization & Record<string, any>;
525 | 		}) => Promise<void | {
526 | 			data: Record<string, any>;
527 | 		}>;
528 | 
529 | 		/**
530 | 		 * A callback that runs after a member is added to an organization
531 | 		 */
532 | 		afterAddMember?: (data: {
533 | 			member: Member & Record<string, any>;
534 | 			user: User & Record<string, any>;
535 | 			organization: Organization & Record<string, any>;
536 | 		}) => Promise<void>;
537 | 
538 | 		/**
539 | 		 * A callback that runs before a member is removed from an organization
540 | 		 */
541 | 		beforeRemoveMember?: (data: {
542 | 			member: Member & Record<string, any>;
543 | 			user: User & Record<string, any>;
544 | 			organization: Organization & Record<string, any>;
545 | 		}) => Promise<void>;
546 | 
547 | 		/**
548 | 		 * A callback that runs after a member is removed from an organization
549 | 		 */
550 | 		afterRemoveMember?: (data: {
551 | 			member: Member & Record<string, any>;
552 | 			user: User & Record<string, any>;
553 | 			organization: Organization & Record<string, any>;
554 | 		}) => Promise<void>;
555 | 
556 | 		/**
557 | 		 * A callback that runs before a member's role is updated
558 | 		 *
559 | 		 * You can return a `data` object to override the default data.
560 | 		 */
561 | 		beforeUpdateMemberRole?: (data: {
562 | 			member: Member & Record<string, any>;
563 | 			newRole: string;
564 | 			user: User & Record<string, any>;
565 | 			organization: Organization & Record<string, any>;
566 | 		}) => Promise<void | {
567 | 			data: {
568 | 				role: string;
569 | 				[key: string]: any;
570 | 			};
571 | 		}>;
572 | 
573 | 		/**
574 | 		 * A callback that runs after a member's role is updated
575 | 		 */
576 | 		afterUpdateMemberRole?: (data: {
577 | 			member: Member & Record<string, any>;
578 | 			previousRole: string;
579 | 			user: User & Record<string, any>;
580 | 			organization: Organization & Record<string, any>;
581 | 		}) => Promise<void>;
582 | 
583 | 		/**
584 | 		 * Invitation hooks
585 | 		 */
586 | 
587 | 		/**
588 | 		 * A callback that runs before an invitation is created
589 | 		 *
590 | 		 * You can return a `data` object to override the default data.
591 | 		 *
592 | 		 * @example
593 | 		 * ```ts
594 | 		 * beforeCreateInvitation: async (data) => {
595 | 		 * 	return {
596 | 		 * 		data: {
597 | 		 * 			...data.invitation,
598 | 		 * 			expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 days
599 | 		 * 		}
600 | 		 * 	};
601 | 		 * }
602 | 		 * ```
603 | 		 */
604 | 		beforeCreateInvitation?: (data: {
605 | 			invitation: {
606 | 				email: string;
607 | 				role: string;
608 | 				organizationId: string;
609 | 				inviterId: string;
610 | 				teamId?: string;
611 | 				[key: string]: any;
612 | 			};
613 | 			inviter: User & Record<string, any>;
614 | 			organization: Organization & Record<string, any>;
615 | 		}) => Promise<void | {
616 | 			data: Record<string, any>;
617 | 		}>;
618 | 
619 | 		/**
620 | 		 * A callback that runs after an invitation is created
621 | 		 */
622 | 		afterCreateInvitation?: (data: {
623 | 			invitation: Invitation & Record<string, any>;
624 | 			inviter: User & Record<string, any>;
625 | 			organization: Organization & Record<string, any>;
626 | 		}) => Promise<void>;
627 | 
628 | 		/**
629 | 		 * A callback that runs before an invitation is accepted
630 | 		 */
631 | 		beforeAcceptInvitation?: (data: {
632 | 			invitation: Invitation & Record<string, any>;
633 | 			user: User & Record<string, any>;
634 | 			organization: Organization & Record<string, any>;
635 | 		}) => Promise<void>;
636 | 
637 | 		/**
638 | 		 * A callback that runs after an invitation is accepted
639 | 		 */
640 | 		afterAcceptInvitation?: (data: {
641 | 			invitation: Invitation & Record<string, any>;
642 | 			member: Member & Record<string, any>;
643 | 			user: User & Record<string, any>;
644 | 			organization: Organization & Record<string, any>;
645 | 		}) => Promise<void>;
646 | 
647 | 		/**
648 | 		 * A callback that runs before an invitation is rejected
649 | 		 */
650 | 		beforeRejectInvitation?: (data: {
651 | 			invitation: Invitation & Record<string, any>;
652 | 			user: User & Record<string, any>;
653 | 			organization: Organization & Record<string, any>;
654 | 		}) => Promise<void>;
655 | 
656 | 		/**
657 | 		 * A callback that runs after an invitation is rejected
658 | 		 */
659 | 		afterRejectInvitation?: (data: {
660 | 			invitation: Invitation & Record<string, any>;
661 | 			user: User & Record<string, any>;
662 | 			organization: Organization & Record<string, any>;
663 | 		}) => Promise<void>;
664 | 
665 | 		/**
666 | 		 * A callback that runs before an invitation is cancelled
667 | 		 */
668 | 		beforeCancelInvitation?: (data: {
669 | 			invitation: Invitation & Record<string, any>;
670 | 			cancelledBy: User & Record<string, any>;
671 | 			organization: Organization & Record<string, any>;
672 | 		}) => Promise<void>;
673 | 
674 | 		/**
675 | 		 * A callback that runs after an invitation is cancelled
676 | 		 */
677 | 		afterCancelInvitation?: (data: {
678 | 			invitation: Invitation & Record<string, any>;
679 | 			cancelledBy: User & Record<string, any>;
680 | 			organization: Organization & Record<string, any>;
681 | 		}) => Promise<void>;
682 | 
683 | 		/**
684 | 		 * Team hooks (when teams are enabled)
685 | 		 */
686 | 
687 | 		/**
688 | 		 * A callback that runs before a team is created
689 | 		 *
690 | 		 * You can return a `data` object to override the default data.
691 | 		 */
692 | 		beforeCreateTeam?: (data: {
693 | 			team: {
694 | 				name: string;
695 | 				organizationId: string;
696 | 				[key: string]: any;
697 | 			};
698 | 			user?: User & Record<string, any>;
699 | 			organization: Organization & Record<string, any>;
700 | 		}) => Promise<void | {
701 | 			data: Record<string, any>;
702 | 		}>;
703 | 
704 | 		/**
705 | 		 * A callback that runs after a team is created
706 | 		 */
707 | 		afterCreateTeam?: (data: {
708 | 			team: Team & Record<string, any>;
709 | 			user?: User & Record<string, any>;
710 | 			organization: Organization & Record<string, any>;
711 | 		}) => Promise<void>;
712 | 
713 | 		/**
714 | 		 * A callback that runs before a team is updated
715 | 		 *
716 | 		 * You can return a `data` object to override the default data.
717 | 		 */
718 | 		beforeUpdateTeam?: (data: {
719 | 			team: Team & Record<string, any>;
720 | 			updates: {
721 | 				name?: string;
722 | 				[key: string]: any;
723 | 			};
724 | 			user: User & Record<string, any>;
725 | 			organization: Organization & Record<string, any>;
726 | 		}) => Promise<void | {
727 | 			data: Record<string, any>;
728 | 		}>;
729 | 
730 | 		/**
731 | 		 * A callback that runs after a team is updated
732 | 		 */
733 | 		afterUpdateTeam?: (data: {
734 | 			team: (Team & Record<string, any>) | null;
735 | 			user: User & Record<string, any>;
736 | 			organization: Organization & Record<string, any>;
737 | 		}) => Promise<void>;
738 | 
739 | 		/**
740 | 		 * A callback that runs before a team is deleted
741 | 		 */
742 | 		beforeDeleteTeam?: (data: {
743 | 			team: Team & Record<string, any>;
744 | 			user?: User & Record<string, any>;
745 | 			organization: Organization & Record<string, any>;
746 | 		}) => Promise<void>;
747 | 
748 | 		/**
749 | 		 * A callback that runs after a team is deleted
750 | 		 */
751 | 		afterDeleteTeam?: (data: {
752 | 			team: Team & Record<string, any>;
753 | 			user?: User & Record<string, any>;
754 | 			organization: Organization & Record<string, any>;
755 | 		}) => Promise<void>;
756 | 
757 | 		/**
758 | 		 * A callback that runs before a member is added to a team
759 | 		 */
760 | 		beforeAddTeamMember?: (data: {
761 | 			teamMember: {
762 | 				teamId: string;
763 | 				userId: string;
764 | 				[key: string]: any;
765 | 			};
766 | 			team: Team & Record<string, any>;
767 | 			user: User & Record<string, any>;
768 | 			organization: Organization & Record<string, any>;
769 | 		}) => Promise<void | {
770 | 			data: Record<string, any>;
771 | 		}>;
772 | 
773 | 		/**
774 | 		 * A callback that runs after a member is added to a team
775 | 		 */
776 | 		afterAddTeamMember?: (data: {
777 | 			teamMember: TeamMember & Record<string, any>;
778 | 			team: Team & Record<string, any>;
779 | 			user: User & Record<string, any>;
780 | 			organization: Organization & Record<string, any>;
781 | 		}) => Promise<void>;
782 | 
783 | 		/**
784 | 		 * A callback that runs before a member is removed from a team
785 | 		 */
786 | 		beforeRemoveTeamMember?: (data: {
787 | 			teamMember: TeamMember & Record<string, any>;
788 | 			team: Team & Record<string, any>;
789 | 			user: User & Record<string, any>;
790 | 			organization: Organization & Record<string, any>;
791 | 		}) => Promise<void>;
792 | 
793 | 		/**
794 | 		 * A callback that runs after a member is removed from a team
795 | 		 */
796 | 		afterRemoveTeamMember?: (data: {
797 | 			teamMember: TeamMember & Record<string, any>;
798 | 			team: Team & Record<string, any>;
799 | 			user: User & Record<string, any>;
800 | 			organization: Organization & Record<string, any>;
801 | 		}) => Promise<void>;
802 | 	};
803 | }
804 | 
```

--------------------------------------------------------------------------------
/docs/components/landing/hero.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import { useEffect, useId, useState } from "react";
  4 | import useMeasure from "react-use-measure";
  5 | import Link from "next/link";
  6 | import clsx from "clsx";
  7 | import { Button } from "@/components/ui/button";
  8 | import { Check, Copy } from "lucide-react";
  9 | import { useTheme } from "next-themes";
 10 | import { Highlight, themes } from "prism-react-renderer";
 11 | import { AnimatePresence, motion, MotionConfig } from "framer-motion";
 12 | import { Builder } from "../builder";
 13 | import { Spotlight } from "./spotlight";
 14 | import { GradientBG } from "./gradient-bg";
 15 | const tabs: { name: "auth.ts" | "client.ts"; code: string }[] = [
 16 | 	{
 17 | 		name: "auth.ts",
 18 | 		code: `export const auth = betterAuth({
 19 | 	database: new Pool({
 20 | 		connectionString: DATABASE_URL,
 21 | 	}),
 22 |     emailAndPassword: {
 23 |         enabled: true,
 24 |     },
 25 | 	plugins: [
 26 | 	  organization(),
 27 |       twoFactor(),
 28 | 	]
 29 | })`,
 30 | 	},
 31 | 	{
 32 | 		name: "client.ts",
 33 | 		code: `const client = createAuthClient({
 34 |     plugins: [passkeyClient()]
 35 | });
 36 |         `,
 37 | 	},
 38 | ];
 39 | 
 40 | function TrafficLightsIcon(props: React.ComponentPropsWithoutRef<"svg">) {
 41 | 	return (
 42 | 		<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
 43 | 			<circle cx="5" cy="5" r="4.5" />
 44 | 			<circle cx="21" cy="5" r="4.5" />
 45 | 			<circle cx="37" cy="5" r="4.5" />
 46 | 		</svg>
 47 | 	);
 48 | }
 49 | 
 50 | export default function Hero() {
 51 | 	return (
 52 | 		<section className="max-h-[40rem] relative w-full flex md:items-center md:justify-center dark:bg-black/[0.96] antialiased bg-grid-white/[0.02] overflow-hidden md:min-h-[40rem]">
 53 | 			<Spotlight />
 54 | 			<div className="overflow-hidden px-2 bg-transparent dark:-mb-32 dark:mt-[-4.75rem] dark:pb-32 dark:pt-[4.75rem] md:w-10/12 mx-auto">
 55 | 				<div className="mx-auto grid lg:max-w-8xl xl:max-w-full grid-cols-1 items-center gap-x-8 gap-y-16 px-4 py-2 lg:grid-cols-2 lg:px-8 lg:py-4 xl:gap-x-16 xl:px-0">
 56 | 					<div className="relative z-10 text-left mt-0 sm:mt-2 md:mt-8 lg:mt-0 md:text-center lg:text-left">
 57 | 						<div className="relative">
 58 | 							<div className="flex flex-col items-center lg:items-start gap-2">
 59 | 								<div className="flex items-end gap-1 mt-2 ">
 60 | 									<div className="flex items-center gap-1">
 61 | 										<svg
 62 | 											xmlns="http://www.w3.org/2000/svg"
 63 | 											width="0.8em"
 64 | 											height="0.8em"
 65 | 											viewBox="0 0 24 24"
 66 | 										>
 67 | 											<path
 68 | 												fill="currentColor"
 69 | 												d="M13 4V2c4.66.5 8.33 4.19 8.85 8.85c.6 5.49-3.35 10.43-8.85 11.03v-2c3.64-.45 6.5-3.32 6.96-6.96A7.994 7.994 0 0 0 13 4m-7.33.2A9.8 9.8 0 0 1 11 2v2.06c-1.43.2-2.78.78-3.9 1.68zM2.05 11a9.8 9.8 0 0 1 2.21-5.33L5.69 7.1A8 8 0 0 0 4.05 11zm2.22 7.33A10.04 10.04 0 0 1 2.06 13h2c.18 1.42.75 2.77 1.63 3.9zm1.4 1.41l1.39-1.37h.04c1.13.88 2.48 1.45 3.9 1.63v2c-1.96-.21-3.82-1-5.33-2.26M12 17l1.56-3.42L17 12l-3.44-1.56L12 7l-1.57 3.44L7 12l3.43 1.58z"
 70 | 											></path>
 71 | 										</svg>
 72 | 										<span className="text-xs text-opacity-75">
 73 | 											Own Your Auth
 74 | 										</span>
 75 | 									</div>
 76 | 								</div>
 77 | 							</div>
 78 | 
 79 | 							<p className="text-zinc-800 dark:text-zinc-300 mt-3 tracking-tight text-2xl md:text-3xl">
 80 | 								The most comprehensive authentication framework for TypeScript.
 81 | 							</p>
 82 | 							<div className="relative mt-2 md:flex items-center gap-2 w-10/12 hidden border border-white/5">
 83 | 								<GradientBG className="w-full flex items-center justify-between">
 84 | 									<div className="w-full flex items-center gap-2">
 85 | 										<p className="md:text-sm text-xs font-mono select-none">
 86 | 											<span>
 87 | 												<span className="text-[#4498c8]">git:</span>
 88 | 												<span className="text-[#F07178]">(main) </span>
 89 | 											</span>
 90 | 											<span className="italic text-amber-600"> x</span>
 91 | 										</p>
 92 | 										<p className=" relative inline tracking-tight opacity-90 md:text-sm text-xs dark:text-white font-mono text-black">
 93 | 											npm add{" "}
 94 | 											<span className="relative dark:text-fuchsia-100 text-fuchsia-950">
 95 | 												better-auth
 96 | 												<span className="absolute h-2 bg-gradient-to-tr from-white via-stone-200 to-stone-300 blur-3xl w-full top-0 left-2"></span>
 97 | 											</span>
 98 | 										</p>
 99 | 									</div>
100 | 									<div className="flex gap-2 items-center">
101 | 										<Link
102 | 											href="https://www.npmjs.com/package/better-auth"
103 | 											target="_blank"
104 | 										>
105 | 											<svg
106 | 												xmlns="http://www.w3.org/2000/svg"
107 | 												width="1em"
108 | 												height="1em"
109 | 												viewBox="0 0 128 128"
110 | 											>
111 | 												<path
112 | 													fill="#cb3837"
113 | 													d="M0 7.062C0 3.225 3.225 0 7.062 0h113.88c3.838 0 7.063 3.225 7.063 7.062v113.88c0 3.838-3.225 7.063-7.063 7.063H7.062c-3.837 0-7.062-3.225-7.062-7.063zm23.69 97.518h40.395l.05-58.532h19.494l-.05 58.581h19.543l.05-78.075l-78.075-.1l-.1 78.126z"
114 | 												></path>
115 | 												<path
116 | 													fill="#fff"
117 | 													d="M25.105 65.52V26.512H40.96c8.72 0 26.274.034 39.008.075l23.153.075v77.866H83.645v-58.54H64.057v58.54H25.105z"
118 | 												></path>
119 | 											</svg>
120 | 										</Link>
121 | 										<Link
122 | 											href="https://github.com/better-auth/better-auth"
123 | 											target="_blank"
124 | 										>
125 | 											<svg
126 | 												xmlns="http://www.w3.org/2000/svg"
127 | 												width="1em"
128 | 												height="1em"
129 | 												viewBox="0 0 256 256"
130 | 											>
131 | 												<g fill="none">
132 | 													<rect
133 | 														width="256"
134 | 														height="256"
135 | 														fill="#242938"
136 | 														rx="60"
137 | 													></rect>
138 | 													<path
139 | 														fill="#fff"
140 | 														d="M128.001 30C72.779 30 28 74.77 28 130.001c0 44.183 28.653 81.667 68.387 94.89c4.997.926 6.832-2.169 6.832-4.81c0-2.385-.093-10.262-.136-18.618c-27.82 6.049-33.69-11.799-33.69-11.799c-4.55-11.559-11.104-14.632-11.104-14.632c-9.073-6.207.684-6.079.684-6.079c10.042.705 15.33 10.305 15.33 10.305c8.919 15.288 23.394 10.868 29.1 8.313c.898-6.464 3.489-10.875 6.349-13.372c-22.211-2.529-45.56-11.104-45.56-49.421c0-10.918 3.906-19.839 10.303-26.842c-1.039-2.519-4.462-12.69.968-26.464c0 0 8.398-2.687 27.508 10.25c7.977-2.215 16.531-3.326 25.03-3.364c8.498.038 17.06 1.149 25.051 3.365c19.087-12.939 27.473-10.25 27.473-10.25c5.443 13.773 2.019 23.945.98 26.463c6.412 7.003 10.292 15.924 10.292 26.842c0 38.409-23.394 46.866-45.662 49.341c3.587 3.104 6.783 9.189 6.783 18.519c0 13.38-.116 24.149-.116 27.443c0 2.661 1.8 5.779 6.869 4.797C199.383 211.64 228 174.169 228 130.001C228 74.771 183.227 30 128.001 30M65.454 172.453c-.22.497-1.002.646-1.714.305c-.726-.326-1.133-1.004-.898-1.502c.215-.512.999-.654 1.722-.311c.727.326 1.141 1.01.89 1.508m4.919 4.389c-.477.443-1.41.237-2.042-.462c-.654-.697-.777-1.629-.293-2.078c.491-.442 1.396-.235 2.051.462c.654.706.782 1.631.284 2.078m3.374 5.616c-.613.426-1.615.027-2.234-.863c-.613-.889-.613-1.955.013-2.383c.621-.427 1.608-.043 2.236.84c.611.904.611 1.971-.015 2.406m5.707 6.504c-.548.604-1.715.442-2.57-.383c-.874-.806-1.118-1.95-.568-2.555c.555-.606 1.729-.435 2.59.383c.868.804 1.133 1.957.548 2.555m7.376 2.195c-.242.784-1.366 1.14-2.499.807c-1.13-.343-1.871-1.26-1.642-2.052c.235-.788 1.364-1.159 2.505-.803c1.13.341 1.871 1.252 1.636 2.048m8.394.932c.028.824-.932 1.508-2.121 1.523c-1.196.027-2.163-.641-2.176-1.452c0-.833.939-1.51 2.134-1.53c1.19-.023 2.163.639 2.163 1.459m8.246-.316c.143.804-.683 1.631-1.864 1.851c-1.161.212-2.236-.285-2.383-1.083c-.144-.825.697-1.651 1.856-1.865c1.183-.205 2.241.279 2.391 1.097"
141 | 													></path>
142 | 												</g>
143 | 											</svg>
144 | 										</Link>
145 | 									</div>
146 | 								</GradientBG>
147 | 							</div>
148 | 
149 | 							{
150 | 								<>
151 | 									<div className="mt-4 flex w-fit flex-col gap-4 font-sans md:flex-row md:justify-center lg:justify-start items-center">
152 | 										<Link
153 | 											href="/docs"
154 | 											className="hover:shadow-sm dark:border-stone-100 dark:hover:shadow-sm border-2 border-black bg-white px-4 py-1.5 text-sm uppercase text-black shadow-[1px_1px_rgba(0,0,0),2px_2px_rgba(0,0,0),3px_3px_rgba(0,0,0),4px_4px_rgba(0,0,0),5px_5px_0px_0px_rgba(0,0,0)] transition duration-200 md:px-8 dark:shadow-[1px_1px_rgba(255,255,255),2px_2px_rgba(255,255,255),3px_3px_rgba(255,255,255),4px_4px_rgba(255,255,255),5px_5px_0px_0px_rgba(255,255,255)]"
155 | 										>
156 | 											Get Started
157 | 										</Link>
158 | 										<Builder />
159 | 									</div>
160 | 								</>
161 | 							}
162 | 						</div>
163 | 					</div>
164 | 
165 | 					<div className="relative hidden md:block lg:static xl:pl-10">
166 | 						<div className="relative">
167 | 							<div className="from-sky-300 via-sky-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-5 blur-lg" />
168 | 							<div className="from-stone-300 via-stone-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-5" />
169 | 							<CodePreview />
170 | 						</div>
171 | 					</div>
172 | 				</div>
173 | 			</div>
174 | 		</section>
175 | 	);
176 | }
177 | 
178 | function CodePreview() {
179 | 	const [currentTab, setCurrentTab] = useState<"auth.ts" | "client.ts">(
180 | 		"auth.ts",
181 | 	);
182 | 
183 | 	const theme = useTheme();
184 | 
185 | 	const code = tabs.find((tab) => tab.name === currentTab)?.code ?? "";
186 | 	const [copyState, setCopyState] = useState(false);
187 | 	const [ref, { height }] = useMeasure();
188 | 	const copyToClipboard = (text: string) => {
189 | 		navigator.clipboard.writeText(text).then(() => {
190 | 			setCopyState(true);
191 | 			setTimeout(() => {
192 | 				setCopyState(false);
193 | 			}, 2000);
194 | 		});
195 | 	};
196 | 
197 | 	const [codeTheme, setCodeTheme] = useState(themes.synthwave84);
198 | 
199 | 	useEffect(() => {
200 | 		setCodeTheme(
201 | 			theme.resolvedTheme === "light" ? themes.oneLight : themes.synthwave84,
202 | 		);
203 | 	}, [theme.resolvedTheme]);
204 | 
205 | 	return (
206 | 		<AnimatePresence initial={false}>
207 | 			<MotionConfig transition={{ duration: 0.5, type: "spring", bounce: 0 }}>
208 | 				<motion.div
209 | 					animate={{ height: height > 0 ? height : undefined }}
210 | 					className="from-stone-100 to-stone-200 dark:to-black/90 dark:via-black dark:from-stone-950/90 relative overflow-hidden rounded-sm bg-gradient-to-tr ring-1 ring-white/10 backdrop-blur-lg"
211 | 				>
212 | 					<div ref={ref}>
213 | 						<div className="absolute -top-px left-0 right-0 h-px" />
214 | 						<div className="absolute -bottom-px left-11 right-20 h-px" />
215 | 						<div className="pl-4 pt-4">
216 | 							<TrafficLightsIcon className="stroke-slate-500/30 h-2.5 w-auto" />
217 | 
218 | 							<div className="mt-4 flex space-x-2 text-xs">
219 | 								{tabs.map((tab) => (
220 | 									<button
221 | 										key={tab.name}
222 | 										onClick={() => setCurrentTab(tab.name)}
223 | 										className={clsx(
224 | 											"relative isolate flex h-6 cursor-pointer items-center justify-center rounded-full px-2.5",
225 | 											currentTab === tab.name
226 | 												? "text-stone-300"
227 | 												: "text-slate-500",
228 | 										)}
229 | 									>
230 | 										{tab.name}
231 | 										{tab.name === currentTab && (
232 | 											<motion.div
233 | 												layoutId="tab-code-preview"
234 | 												className="bg-stone-800 absolute inset-0 -z-10 rounded-full"
235 | 											/>
236 | 										)}
237 | 									</button>
238 | 								))}
239 | 							</div>
240 | 
241 | 							<div className="mt-6 flex flex-col items-start px-1 text-sm">
242 | 								<div className="absolute top-2 right-4">
243 | 									<Button
244 | 										variant="outline"
245 | 										size="icon"
246 | 										className="absolute w-5 border-none bg-transparent h-5 top-2 right-0"
247 | 										onClick={() => copyToClipboard(code)}
248 | 									>
249 | 										{copyState ? (
250 | 											<Check className="h-3 w-3" />
251 | 										) : (
252 | 											<Copy className="h-3 w-3" />
253 | 										)}
254 | 										<span className="sr-only">Copy code</span>
255 | 									</Button>
256 | 								</div>
257 | 								<motion.div
258 | 									initial={{ opacity: 0 }}
259 | 									animate={{ opacity: 1 }}
260 | 									transition={{ duration: 0.5 }}
261 | 									key={currentTab}
262 | 									className="relative flex items-start px-1 text-sm"
263 | 								>
264 | 									<div
265 | 										aria-hidden="true"
266 | 										className="border-slate-300/5 text-slate-600 select-none border-r pr-4 font-mono"
267 | 									>
268 | 										{Array.from({
269 | 											length: code.split("\n").length,
270 | 										}).map((_, index) => (
271 | 											<div key={index}>
272 | 												{(index + 1).toString().padStart(2, "0")}
273 | 												<br />
274 | 											</div>
275 | 										))}
276 | 									</div>
277 | 									<Highlight
278 | 										key={theme.resolvedTheme}
279 | 										code={code}
280 | 										language={"javascript"}
281 | 										theme={{
282 | 											...codeTheme,
283 | 											plain: {
284 | 												backgroundColor: "transparent",
285 | 											},
286 | 										}}
287 | 									>
288 | 										{({
289 | 											className,
290 | 											style,
291 | 											tokens,
292 | 											getLineProps,
293 | 											getTokenProps,
294 | 										}) => (
295 | 											<pre
296 | 												className={clsx(className, "flex overflow-x-auto pb-6")}
297 | 												style={style}
298 | 											>
299 | 												<code className="px-4">
300 | 													{tokens.map((line, lineIndex) => (
301 | 														<div key={lineIndex} {...getLineProps({ line })}>
302 | 															{line.map((token, tokenIndex) => (
303 | 																<span
304 | 																	key={tokenIndex}
305 | 																	{...getTokenProps({ token })}
306 | 																/>
307 | 															))}
308 | 														</div>
309 | 													))}
310 | 												</code>
311 | 											</pre>
312 | 										)}
313 | 									</Highlight>
314 | 								</motion.div>
315 | 								<motion.div layout className="self-end">
316 | 									<Link
317 | 										href="https://demo.better-auth.com"
318 | 										target="_blank"
319 | 										className="shadow-md  border shadow-primary-foreground mb-4 ml-auto mr-4 mt-auto flex cursor-pointer items-center gap-2 px-3 py-1 transition-all ease-in-out hover:opacity-70"
320 | 									>
321 | 										<svg
322 | 											xmlns="http://www.w3.org/2000/svg"
323 | 											width="1em"
324 | 											height="1em"
325 | 											viewBox="0 0 24 24"
326 | 										>
327 | 											<path
328 | 												fill="currentColor"
329 | 												d="M10 20H8V4h2v2h2v3h2v2h2v2h-2v2h-2v3h-2z"
330 | 											></path>
331 | 										</svg>
332 | 										<p className="text-sm">Demo</p>
333 | 									</Link>
334 | 								</motion.div>
335 | 							</div>
336 | 						</div>
337 | 					</div>
338 | 				</motion.div>
339 | 			</MotionConfig>
340 | 		</AnimatePresence>
341 | 	);
342 | }
343 | 
344 | export function HeroBackground(props: React.ComponentPropsWithoutRef<"svg">) {
345 | 	const id = useId();
346 | 	return (
347 | 		<svg
348 | 			aria-hidden="true"
349 | 			viewBox="0 0 668 1069"
350 | 			width={668}
351 | 			height={1069}
352 | 			fill="none"
353 | 			{...props}
354 | 		>
355 | 			<defs>
356 | 				<clipPath id={`${id}-clip-path`}>
357 | 					<path
358 | 						fill="#fff"
359 | 						transform="rotate(-180 334 534.4)"
360 | 						d="M0 0h668v1068.8H0z"
361 | 					/>
362 | 				</clipPath>
363 | 			</defs>
364 | 			<g opacity=".4" clipPath={`url(#${id}-clip-path)`} strokeWidth={4}>
365 | 				<path
366 | 					opacity=".3"
367 | 					d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474"
368 | 					stroke="#334155"
369 | 				/>
370 | 				<path
371 | 					d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588"
372 | 					stroke="#334155"
373 | 				/>
374 | 				<path
375 | 					d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175"
376 | 					stroke="#334155"
377 | 				/>
378 | 				<circle
379 | 					cx="83.5"
380 | 					cy="384.1"
381 | 					r="10.438"
382 | 					transform="rotate(-180 83.5 384.1)"
383 | 					fill="#1E293B"
384 | 					stroke="#334155"
385 | 				/>
386 | 				<circle
387 | 					cx="83.5"
388 | 					cy="200.399"
389 | 					r="10.438"
390 | 					transform="rotate(-180 83.5 200.399)"
391 | 					stroke="#334155"
392 | 				/>
393 | 				<circle
394 | 					cx="83.5"
395 | 					cy="81.412"
396 | 					r="10.438"
397 | 					transform="rotate(-180 83.5 81.412)"
398 | 					stroke="#334155"
399 | 				/>
400 | 				<circle
401 | 					cx="183.699"
402 | 					cy="375.75"
403 | 					r="10.438"
404 | 					transform="rotate(-180 183.699 375.75)"
405 | 					fill="#1E293B"
406 | 					stroke="#334155"
407 | 				/>
408 | 				<circle
409 | 					cx="183.699"
410 | 					cy="563.625"
411 | 					r="10.438"
412 | 					transform="rotate(-180 183.699 563.625)"
413 | 					fill="#1E293B"
414 | 					stroke="#334155"
415 | 				/>
416 | 				<circle
417 | 					cx="384.1"
418 | 					cy="651.3"
419 | 					r="10.438"
420 | 					transform="rotate(-180 384.1 651.3)"
421 | 					fill="#1E293B"
422 | 					stroke="#334155"
423 | 				/>
424 | 				<circle
425 | 					cx="484.301"
426 | 					cy="574.062"
427 | 					r="10.438"
428 | 					transform="rotate(-180 484.301 574.062)"
429 | 					fill="#0EA5E9"
430 | 					fillOpacity=".42"
431 | 					stroke="#0EA5E9"
432 | 				/>
433 | 				<circle
434 | 					cx="384.1"
435 | 					cy="749.412"
436 | 					r="10.438"
437 | 					transform="rotate(-180 384.1 749.412)"
438 | 					fill="#1E293B"
439 | 					stroke="#334155"
440 | 				/>
441 | 				<circle
442 | 					cx="384.1"
443 | 					cy="1027.05"
444 | 					r="10.438"
445 | 					transform="rotate(-180 384.1 1027.05)"
446 | 					stroke="#334155"
447 | 				/>
448 | 				<circle
449 | 					cx="283.9"
450 | 					cy="924.763"
451 | 					r="10.438"
452 | 					transform="rotate(-180 283.9 924.763)"
453 | 					stroke="#334155"
454 | 				/>
455 | 				<circle
456 | 					cx="183.699"
457 | 					cy="870.487"
458 | 					r="10.438"
459 | 					transform="rotate(-180 183.699 870.487)"
460 | 					stroke="#334155"
461 | 				/>
462 | 				<circle
463 | 					cx="283.9"
464 | 					cy="738.975"
465 | 					r="10.438"
466 | 					transform="rotate(-180 283.9 738.975)"
467 | 					fill="#1E293B"
468 | 					stroke="#334155"
469 | 				/>
470 | 				<circle
471 | 					cx="83.5"
472 | 					cy="695.138"
473 | 					r="10.438"
474 | 					transform="rotate(-180 83.5 695.138)"
475 | 					fill="#1E293B"
476 | 					stroke="#334155"
477 | 				/>
478 | 				<circle
479 | 					cx="83.5"
480 | 					cy="484.3"
481 | 					r="10.438"
482 | 					transform="rotate(-180 83.5 484.3)"
483 | 					fill="#0EA5E9"
484 | 					fillOpacity=".42"
485 | 					stroke="#0EA5E9"
486 | 				/>
487 | 				<circle
488 | 					cx="484.301"
489 | 					cy="432.112"
490 | 					r="10.438"
491 | 					transform="rotate(-180 484.301 432.112)"
492 | 					fill="#1E293B"
493 | 					stroke="#334155"
494 | 				/>
495 | 				<circle
496 | 					cx="584.5"
497 | 					cy="432.112"
498 | 					r="10.438"
499 | 					transform="rotate(-180 584.5 432.112)"
500 | 					fill="#1E293B"
501 | 					stroke="#334155"
502 | 				/>
503 | 				<circle
504 | 					cx="584.5"
505 | 					cy="642.95"
506 | 					r="10.438"
507 | 					transform="rotate(-180 584.5 642.95)"
508 | 					fill="#1E293B"
509 | 					stroke="#334155"
510 | 				/>
511 | 				<circle
512 | 					cx="484.301"
513 | 					cy="851.699"
514 | 					r="10.438"
515 | 					transform="rotate(-180 484.301 851.699)"
516 | 					stroke="#334155"
517 | 				/>
518 | 				<circle
519 | 					cx="384.1"
520 | 					cy="256.763"
521 | 					r="10.438"
522 | 					transform="rotate(-180 384.1 256.763)"
523 | 					stroke="#334155"
524 | 				/>
525 | 			</g>
526 | 		</svg>
527 | 	);
528 | }
529 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/siwe/siwe.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect } from "vitest";
  2 | import { getTestInstance } from "../../test-utils/test-instance";
  3 | import { siwe } from "./index";
  4 | import { siweClient } from "./client";
  5 | 
  6 | describe("siwe", async (it) => {
  7 | 	const walletAddress = "0x000000000000000000000000000000000000dEaD";
  8 | 	const domain = "example.com";
  9 | 	const chainId = 1; // Ethereum mainnet
 10 | 
 11 | 	it("should generate a valid nonce for a valid public key", async () => {
 12 | 		const { client } = await getTestInstance(
 13 | 			{
 14 | 				plugins: [
 15 | 					siwe({
 16 | 						domain,
 17 | 						async getNonce() {
 18 | 							return "A1b2C3d4E5f6G7h8J";
 19 | 						},
 20 | 						async verifyMessage({ message, signature }) {
 21 | 							return (
 22 | 								signature === "valid_signature" && message === "valid_message"
 23 | 							);
 24 | 						},
 25 | 					}),
 26 | 				],
 27 | 			},
 28 | 			{
 29 | 				clientOptions: {
 30 | 					plugins: [siweClient()],
 31 | 				},
 32 | 			},
 33 | 		);
 34 | 		const { data } = await client.siwe.nonce({ walletAddress, chainId });
 35 | 		// to be of type string
 36 | 		expect(typeof data?.nonce).toBe("string");
 37 | 		// to be 17 alphanumeric characters (96 bits of entropy)
 38 | 		expect(data?.nonce).toMatch(/^[a-zA-Z0-9]{17}$/);
 39 | 	});
 40 | 
 41 | 	it("should generate a valid nonce with default chainId", async () => {
 42 | 		const { client } = await getTestInstance(
 43 | 			{
 44 | 				plugins: [
 45 | 					siwe({
 46 | 						domain,
 47 | 						async getNonce() {
 48 | 							return "A1b2C3d4E5f6G7h8J";
 49 | 						},
 50 | 						async verifyMessage({ message, signature }) {
 51 | 							return (
 52 | 								signature === "valid_signature" && message === "valid_message"
 53 | 							);
 54 | 						},
 55 | 					}),
 56 | 				],
 57 | 			},
 58 | 			{
 59 | 				clientOptions: {
 60 | 					plugins: [siweClient()],
 61 | 				},
 62 | 			},
 63 | 		);
 64 | 		// Test without chainId (should default to 1)
 65 | 		const { data } = await client.siwe.nonce({ walletAddress });
 66 | 		expect(typeof data?.nonce).toBe("string");
 67 | 		expect(data?.nonce).toMatch(/^[a-zA-Z0-9]{17}$/);
 68 | 	});
 69 | 
 70 | 	it("should reject verification if nonce is missing", async () => {
 71 | 		const { client } = await getTestInstance(
 72 | 			{
 73 | 				plugins: [
 74 | 					siwe({
 75 | 						domain,
 76 | 						async getNonce() {
 77 | 							return "A1b2C3d4E5f6G7h8J";
 78 | 						},
 79 | 						async verifyMessage({ message, signature }) {
 80 | 							return (
 81 | 								signature === "valid_signature" && message === "valid_message"
 82 | 							);
 83 | 						},
 84 | 					}),
 85 | 				],
 86 | 			},
 87 | 			{
 88 | 				clientOptions: {
 89 | 					plugins: [siweClient()],
 90 | 				},
 91 | 			},
 92 | 		);
 93 | 		const { error } = await client.siwe.verify({
 94 | 			message: "valid_message",
 95 | 			signature: "valid_signature",
 96 | 			walletAddress,
 97 | 			chainId,
 98 | 		});
 99 | 
100 | 		expect(error).toBeDefined();
101 | 		expect(error?.status).toBe(401);
102 | 		expect(error?.code).toBe("UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE");
103 | 		expect(error?.message).toMatch(/nonce/i);
104 | 	});
105 | 
106 | 	it("should reject invalid public key", async () => {
107 | 		const { client } = await getTestInstance(
108 | 			{
109 | 				plugins: [
110 | 					siwe({
111 | 						domain,
112 | 						async getNonce() {
113 | 							return "A1b2C3d4E5f6G7h8J";
114 | 						},
115 | 						async verifyMessage({ message, signature }) {
116 | 							return (
117 | 								signature === "valid_signature" && message === "valid_message"
118 | 							);
119 | 						},
120 | 					}),
121 | 				],
122 | 			},
123 | 			{
124 | 				clientOptions: {
125 | 					plugins: [siweClient()],
126 | 				},
127 | 			},
128 | 		);
129 | 		const { error } = await client.siwe.nonce({ walletAddress: "invalid" });
130 | 		expect(error).toBeDefined();
131 | 		expect(error?.status).toBe(400);
132 | 		expect(error?.message).toBe("Invalid body parameters");
133 | 	});
134 | 
135 | 	it("should reject verification with invalid signature", async () => {
136 | 		const { client } = await getTestInstance(
137 | 			{
138 | 				plugins: [
139 | 					siwe({
140 | 						domain,
141 | 						async getNonce() {
142 | 							return "A1b2C3d4E5f6G7h8J";
143 | 						},
144 | 						async verifyMessage({ message, signature }) {
145 | 							return (
146 | 								signature === "valid_signature" && message === "valid_message"
147 | 							);
148 | 						},
149 | 					}),
150 | 				],
151 | 			},
152 | 			{
153 | 				clientOptions: {
154 | 					plugins: [siweClient()],
155 | 				},
156 | 			},
157 | 		);
158 | 		const { error } = await client.siwe.verify({
159 | 			message: "Sign in with Ethereum.",
160 | 			signature: "invalid_signature",
161 | 			walletAddress,
162 | 		});
163 | 		expect(error).toBeDefined();
164 | 		expect(error?.status).toBe(401);
165 | 	});
166 | 
167 | 	it("should reject invalid walletAddress format", async () => {
168 | 		const { client } = await getTestInstance(
169 | 			{
170 | 				plugins: [
171 | 					siwe({
172 | 						domain,
173 | 						async getNonce() {
174 | 							return "A1b2C3d4E5f6G7h8J";
175 | 						},
176 | 						async verifyMessage({ message, signature }) {
177 | 							return (
178 | 								signature === "valid_signature" && message === "valid_message"
179 | 							);
180 | 						},
181 | 					}),
182 | 				],
183 | 			},
184 | 			{
185 | 				clientOptions: {
186 | 					plugins: [siweClient()],
187 | 				},
188 | 			},
189 | 		);
190 | 		const { error } = await client.siwe.nonce({
191 | 			walletAddress: "not_a_valid_key",
192 | 		});
193 | 		expect(error).toBeDefined();
194 | 		expect(error?.status).toBe(400);
195 | 	});
196 | 
197 | 	it("should reject invalid message", async () => {
198 | 		const { client } = await getTestInstance(
199 | 			{
200 | 				plugins: [
201 | 					siwe({
202 | 						domain,
203 | 						async getNonce() {
204 | 							return "A1b2C3d4E5f6G7h8J";
205 | 						},
206 | 						async verifyMessage({ message, signature }) {
207 | 							return (
208 | 								signature === "valid_signature" && message === "valid_message"
209 | 							);
210 | 						},
211 | 					}),
212 | 				],
213 | 			},
214 | 			{
215 | 				clientOptions: {
216 | 					plugins: [siweClient()],
217 | 				},
218 | 			},
219 | 		);
220 | 		const { error } = await client.siwe.verify({
221 | 			message: "invalid_message",
222 | 			signature: "valid_signature",
223 | 			walletAddress,
224 | 		});
225 | 		expect(error).toBeDefined();
226 | 		expect(error?.status).toBe(401);
227 | 	});
228 | 
229 | 	it("should reject verification without email when anonymous is false", async () => {
230 | 		const { client } = await getTestInstance(
231 | 			{
232 | 				plugins: [
233 | 					siwe({
234 | 						domain,
235 | 						anonymous: false,
236 | 						async getNonce() {
237 | 							return "A1b2C3d4E5f6G7h8J";
238 | 						},
239 | 						async verifyMessage({ message, signature }) {
240 | 							return (
241 | 								signature === "valid_signature" && message === "valid_message"
242 | 							);
243 | 						},
244 | 					}),
245 | 				],
246 | 			},
247 | 			{
248 | 				clientOptions: {
249 | 					plugins: [siweClient()],
250 | 				},
251 | 			},
252 | 		);
253 | 
254 | 		const { error } = await client.siwe.verify({
255 | 			message: "valid_message",
256 | 			signature: "valid_signature",
257 | 			walletAddress,
258 | 			email: undefined,
259 | 		});
260 | 		expect(error).toBeDefined();
261 | 		expect(error?.status).toBe(400);
262 | 		expect(error?.message).toBe("Invalid body parameters");
263 | 	});
264 | 
265 | 	it("should accept verification with email when anonymous is false", async () => {
266 | 		const { client } = await getTestInstance(
267 | 			{
268 | 				plugins: [
269 | 					siwe({
270 | 						domain,
271 | 						anonymous: false,
272 | 						async getNonce() {
273 | 							return "A1b2C3d4E5f6G7h8J";
274 | 						},
275 | 						async verifyMessage({ message, signature }) {
276 | 							return (
277 | 								signature === "valid_signature" && message === "valid_message"
278 | 							);
279 | 						},
280 | 					}),
281 | 				],
282 | 			},
283 | 			{
284 | 				clientOptions: {
285 | 					plugins: [siweClient()],
286 | 				},
287 | 			},
288 | 		);
289 | 
290 | 		await client.siwe.nonce({ walletAddress, chainId });
291 | 
292 | 		const { data, error } = await client.siwe.verify({
293 | 			message: "valid_message",
294 | 			signature: "valid_signature",
295 | 			walletAddress,
296 | 			chainId,
297 | 			email: "[email protected]",
298 | 		});
299 | 		expect(error).toBeNull();
300 | 		expect(data?.success).toBe(true);
301 | 	});
302 | 
303 | 	it("should reject invalid email format when anonymous is false", async () => {
304 | 		const { client } = await getTestInstance(
305 | 			{
306 | 				plugins: [
307 | 					siwe({
308 | 						domain,
309 | 						anonymous: false,
310 | 						async getNonce() {
311 | 							return "A1b2C3d4E5f6G7h8J";
312 | 						},
313 | 						async verifyMessage({ message, signature }) {
314 | 							return (
315 | 								signature === "valid_signature" && message === "valid_message"
316 | 							);
317 | 						},
318 | 					}),
319 | 				],
320 | 			},
321 | 			{
322 | 				clientOptions: {
323 | 					plugins: [siweClient()],
324 | 				},
325 | 			},
326 | 		);
327 | 
328 | 		const { error } = await client.siwe.verify({
329 | 			message: "valid_message",
330 | 			signature: "valid_signature",
331 | 			walletAddress,
332 | 			email: "not-an-email",
333 | 		});
334 | 		expect(error).toBeDefined();
335 | 		expect(error?.status).toBe(400);
336 | 		expect(error?.message).toBe("Invalid body parameters");
337 | 	});
338 | 
339 | 	it("should allow verification without email when anonymous is true", async () => {
340 | 		const { client } = await getTestInstance(
341 | 			{
342 | 				plugins: [
343 | 					siwe({
344 | 						domain,
345 | 						// anonymous: true by default
346 | 						async getNonce() {
347 | 							return "A1b2C3d4E5f6G7h8J";
348 | 						},
349 | 						async verifyMessage({ message, signature }) {
350 | 							return (
351 | 								signature === "valid_signature" && message === "valid_message"
352 | 							);
353 | 						},
354 | 					}),
355 | 				],
356 | 			},
357 | 			{
358 | 				clientOptions: {
359 | 					plugins: [siweClient()],
360 | 				},
361 | 			},
362 | 		);
363 | 
364 | 		await client.siwe.nonce({ walletAddress, chainId });
365 | 		const { data, error } = await client.siwe.verify({
366 | 			message: "valid_message",
367 | 			signature: "valid_signature",
368 | 			walletAddress,
369 | 			chainId,
370 | 		});
371 | 		expect(error).toBeNull();
372 | 		expect(data?.success).toBe(true);
373 | 	});
374 | 
375 | 	it("should not allow nonce reuse", async () => {
376 | 		const { client } = await getTestInstance(
377 | 			{
378 | 				plugins: [
379 | 					siwe({
380 | 						domain,
381 | 						async getNonce() {
382 | 							return "A1b2C3d4E5f6G7h8J";
383 | 						},
384 | 						async verifyMessage({ message, signature }) {
385 | 							return (
386 | 								signature === "valid_signature" && message === "valid_message"
387 | 							);
388 | 						},
389 | 					}),
390 | 				],
391 | 			},
392 | 			{
393 | 				clientOptions: { plugins: [siweClient()] },
394 | 			},
395 | 		);
396 | 
397 | 		await client.siwe.nonce({ walletAddress, chainId });
398 | 		const first = await client.siwe.verify({
399 | 			message: "valid_message",
400 | 			signature: "valid_signature",
401 | 			walletAddress,
402 | 			chainId,
403 | 		});
404 | 		expect(first.error).toBeNull();
405 | 		expect(first.data?.success).toBe(true);
406 | 
407 | 		// Try to verify again with the same nonce
408 | 		const second = await client.siwe.verify({
409 | 			message: "valid_message",
410 | 			signature: "valid_signature",
411 | 			walletAddress,
412 | 			chainId,
413 | 		});
414 | 		expect(second.error).toBeDefined();
415 | 		expect(second.error?.status).toBe(401);
416 | 		expect(second.error?.code).toBe("UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE");
417 | 	});
418 | 
419 | 	it("should reject empty string email when anonymous is false", async () => {
420 | 		const { client } = await getTestInstance(
421 | 			{
422 | 				plugins: [
423 | 					siwe({
424 | 						domain,
425 | 						anonymous: false,
426 | 						async getNonce() {
427 | 							return "A1b2C3d4E5f6G7h8J";
428 | 						},
429 | 						async verifyMessage({ message, signature }) {
430 | 							return (
431 | 								signature === "valid_signature" && message === "valid_message"
432 | 							);
433 | 						},
434 | 					}),
435 | 				],
436 | 			},
437 | 			{
438 | 				clientOptions: { plugins: [siweClient()] },
439 | 			},
440 | 		);
441 | 
442 | 		await client.siwe.nonce({ walletAddress, chainId });
443 | 		const { error } = await client.siwe.verify({
444 | 			message: "valid_message",
445 | 			signature: "valid_signature",
446 | 			walletAddress,
447 | 			chainId,
448 | 			email: "",
449 | 		});
450 | 		expect(error).toBeDefined();
451 | 		expect(error?.status).toBe(400);
452 | 		expect(error?.message).toBe("Invalid body parameters");
453 | 	});
454 | 
455 | 	it("should store and return the wallet address in checksum format", async () => {
456 | 		const { client, auth } = await getTestInstance(
457 | 			{
458 | 				plugins: [
459 | 					siwe({
460 | 						domain,
461 | 						async getNonce() {
462 | 							return "A1b2C3d4E5f6G7h8J";
463 | 						},
464 | 						async verifyMessage({ message, signature }) {
465 | 							return (
466 | 								signature === "valid_signature" && message === "valid_message"
467 | 							);
468 | 						},
469 | 					}),
470 | 				],
471 | 			},
472 | 			{
473 | 				clientOptions: { plugins: [siweClient()] },
474 | 			},
475 | 		);
476 | 
477 | 		// Use lowercase address
478 | 		await client.siwe.nonce({
479 | 			walletAddress: walletAddress.toLowerCase(),
480 | 			chainId,
481 | 		});
482 | 		const { data } = await client.siwe.verify({
483 | 			message: "valid_message",
484 | 			signature: "valid_signature",
485 | 			walletAddress: walletAddress.toLowerCase(),
486 | 			chainId,
487 | 		});
488 | 		expect(data?.success).toBe(true);
489 | 
490 | 		// Fetch wallet address from the adapter
491 | 		const walletAddresses: any[] = await (await auth.$context).adapter.findMany(
492 | 			{
493 | 				model: "walletAddress",
494 | 				where: [{ field: "address", operator: "eq", value: walletAddress }],
495 | 			},
496 | 		);
497 | 		expect(walletAddresses.length).toBe(1);
498 | 		expect(walletAddresses[0]?.address).toBe(walletAddress); // checksummed
499 | 
500 | 		// Try with uppercase address, should not create a new wallet address entry
501 | 		await client.siwe.nonce({
502 | 			walletAddress: walletAddress.toUpperCase(),
503 | 			chainId,
504 | 		});
505 | 		const { data: data2, error: error2 } = await client.siwe.verify({
506 | 			message: "valid_message",
507 | 			signature: "valid_signature",
508 | 			walletAddress: walletAddress.toUpperCase(),
509 | 			chainId,
510 | 		});
511 | 		expect(data2?.success).toBe(true); // Should succeed with existing address
512 | 
513 | 		const walletAddressesAfter = await (await auth.$context).adapter.findMany({
514 | 			model: "walletAddress",
515 | 			where: [{ field: "address", operator: "eq", value: walletAddress }],
516 | 		});
517 | 		expect(walletAddressesAfter.length).toBe(1); // Still only one wallet address entry
518 | 	});
519 | 
520 | 	it("should reject duplicate wallet address entries", async () => {
521 | 		const { client, auth } = await getTestInstance(
522 | 			{
523 | 				plugins: [
524 | 					siwe({
525 | 						domain,
526 | 						async getNonce() {
527 | 							return "A1b2C3d4E5f6G7h8J";
528 | 						},
529 | 						async verifyMessage({ message, signature }) {
530 | 							return (
531 | 								signature === "valid_signature" && message === "valid_message"
532 | 							);
533 | 						},
534 | 					}),
535 | 				],
536 | 			},
537 | 			{ clientOptions: { plugins: [siweClient()] } },
538 | 		);
539 | 
540 | 		const testAddress = "0x000000000000000000000000000000000000dEaD";
541 | 		const testChainId = 1;
542 | 
543 | 		// First user successfully creates account with wallet address
544 | 		await client.siwe.nonce({
545 | 			walletAddress: testAddress,
546 | 			chainId: testChainId,
547 | 		});
548 | 		const firstUser = await client.siwe.verify({
549 | 			message: "valid_message",
550 | 			signature: "valid_signature",
551 | 			walletAddress: testAddress,
552 | 			chainId: testChainId,
553 | 		});
554 | 		expect(firstUser.error).toBeNull();
555 | 		expect(firstUser.data?.success).toBe(true);
556 | 
557 | 		// Verify wallet address record was created
558 | 		const walletAddresses: any[] = await (await auth.$context).adapter.findMany(
559 | 			{
560 | 				model: "walletAddress",
561 | 				where: [
562 | 					{ field: "address", operator: "eq", value: testAddress },
563 | 					{ field: "chainId", operator: "eq", value: testChainId },
564 | 				],
565 | 			},
566 | 		);
567 | 		expect(walletAddresses.length).toBe(1);
568 | 		expect(walletAddresses[0]?.address).toBe(testAddress);
569 | 		expect(walletAddresses[0]?.chainId).toBe(testChainId);
570 | 		expect(walletAddresses[0]?.isPrimary).toBe(true);
571 | 
572 | 		// Second attempt with same address + chainId should use existing user
573 | 		await client.siwe.nonce({
574 | 			walletAddress: testAddress,
575 | 			chainId: testChainId,
576 | 		});
577 | 		const secondUser = await client.siwe.verify({
578 | 			message: "valid_message",
579 | 			signature: "valid_signature",
580 | 			walletAddress: testAddress,
581 | 			chainId: testChainId,
582 | 		});
583 | 		expect(secondUser.error).toBeNull();
584 | 		expect(secondUser.data?.success).toBe(true);
585 | 		expect(secondUser.data?.user.id).toBe(firstUser.data?.user.id); // Same user ID
586 | 
587 | 		// Verify no duplicate wallet address records were created
588 | 		const walletAddressesAfter: any[] = await (
589 | 			await auth.$context
590 | 		).adapter.findMany({
591 | 			model: "walletAddress",
592 | 			where: [
593 | 				{ field: "address", operator: "eq", value: testAddress },
594 | 				{ field: "chainId", operator: "eq", value: testChainId },
595 | 			],
596 | 		});
597 | 		expect(walletAddressesAfter.length).toBe(1); // Still only one record
598 | 
599 | 		// Verify total user count (should be only 1 user created)
600 | 		const allUsers: any[] = await (await auth.$context).adapter.findMany({
601 | 			model: "user",
602 | 		});
603 | 		const usersWithTestAddress = allUsers.filter((user) =>
604 | 			walletAddressesAfter.some((wa) => wa.userId === user.id),
605 | 		);
606 | 		expect(usersWithTestAddress.length).toBe(1); // Only one user should have this address
607 | 	});
608 | 
609 | 	it("should support custom schema with mergeSchema", async () => {
610 | 		const { client, auth } = await getTestInstance(
611 | 			{
612 | 				logger: {
613 | 					level: "debug",
614 | 				},
615 | 				plugins: [
616 | 					siwe({
617 | 						domain,
618 | 						async getNonce() {
619 | 							return "A1b2C3d4E5f6G7h8J";
620 | 						},
621 | 						async verifyMessage({ message, signature }) {
622 | 							return (
623 | 								signature === "valid_signature" && message === "valid_message"
624 | 							);
625 | 						},
626 | 						schema: {
627 | 							walletAddress: {
628 | 								modelName: "wallet_address",
629 | 								fields: {
630 | 									userId: "user_id",
631 | 									address: "wallet_address",
632 | 									chainId: "chain_id",
633 | 									isPrimary: "is_primary",
634 | 									createdAt: "created_at",
635 | 								},
636 | 							},
637 | 						},
638 | 					}),
639 | 				],
640 | 			},
641 | 			{ clientOptions: { plugins: [siweClient()] } },
642 | 		);
643 | 
644 | 		const testAddress = "0x000000000000000000000000000000000000dEaD";
645 | 		const testChainId = 1;
646 | 
647 | 		// Create account with custom schema
648 | 		await client.siwe.nonce({
649 | 			walletAddress: testAddress,
650 | 			chainId: testChainId,
651 | 		});
652 | 		const result = await client.siwe.verify({
653 | 			message: "valid_message",
654 | 			signature: "valid_signature",
655 | 			walletAddress: testAddress,
656 | 			chainId: testChainId,
657 | 		});
658 | 		expect(result.error).toBeNull();
659 | 		expect(result.data?.success).toBe(true);
660 | 		const context = await auth.$context;
661 | 
662 | 		const walletAddresses: any[] = await context.adapter.findMany({
663 | 			model: "walletAddress",
664 | 			where: [
665 | 				{ field: "address", operator: "eq", value: testAddress },
666 | 				{ field: "chainId", operator: "eq", value: testChainId },
667 | 			],
668 | 		});
669 | 		expect(walletAddresses.length).toBe(1);
670 | 		expect(walletAddresses[0]?.address).toBe(testAddress);
671 | 		expect(walletAddresses[0]?.chainId).toBe(testChainId);
672 | 		expect(walletAddresses[0]?.isPrimary).toBe(true);
673 | 		expect(walletAddresses[0]?.userId).toBeDefined();
674 | 		expect(walletAddresses[0]?.createdAt).toBeDefined();
675 | 	});
676 | 
677 | 	it("should allow same address on different chains for same user", async () => {
678 | 		const { client, auth } = await getTestInstance(
679 | 			{
680 | 				plugins: [
681 | 					siwe({
682 | 						domain,
683 | 						async getNonce() {
684 | 							return "A1b2C3d4E5f6G7h8J";
685 | 						},
686 | 						async verifyMessage({ message, signature }) {
687 | 							return (
688 | 								signature === "valid_signature" && message === "valid_message"
689 | 							);
690 | 						},
691 | 					}),
692 | 				],
693 | 			},
694 | 			{ clientOptions: { plugins: [siweClient()] } },
695 | 		);
696 | 
697 | 		const testAddress = "0x000000000000000000000000000000000000dEaD";
698 | 		const chainId1 = 1; // Ethereum
699 | 		const chainId2 = 137; // Polygon
700 | 
701 | 		// First authentication on Ethereum
702 | 		await client.siwe.nonce({ walletAddress: testAddress, chainId: chainId1 });
703 | 		const ethereumAuth = await client.siwe.verify({
704 | 			message: "valid_message",
705 | 			signature: "valid_signature",
706 | 			walletAddress: testAddress,
707 | 			chainId: chainId1,
708 | 		});
709 | 		expect(ethereumAuth.error).toBeNull();
710 | 		expect(ethereumAuth.data?.success).toBe(true);
711 | 
712 | 		// Second authentication on Polygon with same address
713 | 		await client.siwe.nonce({ walletAddress: testAddress, chainId: chainId2 });
714 | 		const polygonAuth = await client.siwe.verify({
715 | 			message: "valid_message",
716 | 			signature: "valid_signature",
717 | 			walletAddress: testAddress,
718 | 			chainId: chainId2,
719 | 		});
720 | 		expect(polygonAuth.error).toBeNull();
721 | 		expect(polygonAuth.data?.success).toBe(true);
722 | 		expect(polygonAuth.data?.user.id).toBe(ethereumAuth.data?.user.id); // Same user
723 | 
724 | 		// Verify both wallet address records exist
725 | 		const allWalletAddresses: any[] = await (
726 | 			await auth.$context
727 | 		).adapter.findMany({
728 | 			model: "walletAddress",
729 | 			where: [{ field: "address", operator: "eq", value: testAddress }],
730 | 		});
731 | 		expect(allWalletAddresses.length).toBe(2);
732 | 
733 | 		const ethereumRecord = allWalletAddresses.find(
734 | 			(wa) => wa.chainId === chainId1,
735 | 		);
736 | 		const polygonRecord = allWalletAddresses.find(
737 | 			(wa) => wa.chainId === chainId2,
738 | 		);
739 | 
740 | 		expect(ethereumRecord).toBeDefined();
741 | 		expect(polygonRecord).toBeDefined();
742 | 		expect(ethereumRecord?.isPrimary).toBe(true); // First address is primary
743 | 		expect(polygonRecord?.isPrimary).toBe(false); // Second address is not primary
744 | 		expect(ethereumRecord?.userId).toBe(polygonRecord?.userId); // Same user ID
745 | 	});
746 | });
747 | 
```
Page 41/68FirstPrevNextLast