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

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/device-authorization/device-authorization.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, vi } from "vitest";
  2 | import { getTestInstance } from "../../test-utils/test-instance";
  3 | import { $deviceAuthorizationOptionsSchema, deviceAuthorization } from ".";
  4 | import { deviceAuthorizationClient } from "./client";
  5 | import type { DeviceCode } from "./schema";
  6 | 
  7 | describe("device authorization plugin input validation", () => {
  8 | 	it("basic validation", async () => {
  9 | 		const options = $deviceAuthorizationOptionsSchema.parse({});
 10 | 		expect(options).toMatchInlineSnapshot(`
 11 | 			{
 12 | 			  "deviceCodeLength": 40,
 13 | 			  "expiresIn": "30m",
 14 | 			  "interval": "5s",
 15 | 			  "userCodeLength": 8,
 16 | 			}
 17 | 		`);
 18 | 	});
 19 | 
 20 | 	it("should validate custom options", async () => {
 21 | 		const options = $deviceAuthorizationOptionsSchema.parse({
 22 | 			expiresIn: 60 * 1000,
 23 | 			interval: 2 * 1000,
 24 | 			deviceCodeLength: 50,
 25 | 			userCodeLength: 10,
 26 | 		});
 27 | 		expect(options).toMatchInlineSnapshot(`
 28 | 			{
 29 | 			  "deviceCodeLength": 50,
 30 | 			  "expiresIn": 60000,
 31 | 			  "interval": 2000,
 32 | 			  "userCodeLength": 10,
 33 | 			}
 34 | 		`);
 35 | 	});
 36 | });
 37 | 
 38 | describe("client validation", async () => {
 39 | 	const validClients = ["valid-client-1", "valid-client-2"];
 40 | 
 41 | 	const { auth } = await getTestInstance({
 42 | 		plugins: [
 43 | 			deviceAuthorization({
 44 | 				validateClient: async (clientId) => {
 45 | 					return validClients.includes(clientId);
 46 | 				},
 47 | 			}),
 48 | 		],
 49 | 	});
 50 | 
 51 | 	it("should reject invalid client in device code request", async () => {
 52 | 		await expect(
 53 | 			auth.api.deviceCode({
 54 | 				body: {
 55 | 					client_id: "invalid-client",
 56 | 				},
 57 | 			}),
 58 | 		).rejects.toMatchObject({
 59 | 			body: {
 60 | 				error: "invalid_client",
 61 | 				error_description: "Invalid client ID",
 62 | 			},
 63 | 		});
 64 | 	});
 65 | 
 66 | 	it("should accept valid client in device code request", async () => {
 67 | 		const response = await auth.api.deviceCode({
 68 | 			body: {
 69 | 				client_id: "valid-client-1",
 70 | 			},
 71 | 		});
 72 | 		expect(response.device_code).toBeDefined();
 73 | 	});
 74 | 
 75 | 	it("should reject invalid client in token request", async () => {
 76 | 		const { device_code } = await auth.api.deviceCode({
 77 | 			body: {
 78 | 				client_id: "valid-client-1",
 79 | 			},
 80 | 		});
 81 | 
 82 | 		await expect(
 83 | 			auth.api.deviceToken({
 84 | 				body: {
 85 | 					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
 86 | 					device_code,
 87 | 					client_id: "invalid-client",
 88 | 				},
 89 | 			}),
 90 | 		).rejects.toMatchObject({
 91 | 			body: {
 92 | 				error: "invalid_grant",
 93 | 				error_description: "Invalid client ID",
 94 | 			},
 95 | 		});
 96 | 	});
 97 | 
 98 | 	it("should reject mismatched client_id in token request", async () => {
 99 | 		const { device_code } = await auth.api.deviceCode({
100 | 			body: {
101 | 				client_id: "valid-client-1",
102 | 			},
103 | 		});
104 | 
105 | 		await expect(
106 | 			auth.api.deviceToken({
107 | 				body: {
108 | 					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
109 | 					device_code,
110 | 					client_id: "valid-client-2",
111 | 				},
112 | 			}),
113 | 		).rejects.toMatchObject({
114 | 			body: {
115 | 				error: "invalid_grant",
116 | 				error_description: "Client ID mismatch",
117 | 			},
118 | 		});
119 | 	});
120 | });
121 | 
122 | describe("device authorization flow", async () => {
123 | 	const { auth, client, sessionSetter, signInWithTestUser } =
124 | 		await getTestInstance(
125 | 			{
126 | 				plugins: [
127 | 					deviceAuthorization({
128 | 						expiresIn: "5min",
129 | 						interval: "2s",
130 | 					}),
131 | 				],
132 | 			},
133 | 			{
134 | 				clientOptions: {
135 | 					plugins: [deviceAuthorizationClient()],
136 | 				},
137 | 			},
138 | 		);
139 | 
140 | 	describe("device code request", () => {
141 | 		it("should generate device and user codes", async () => {
142 | 			const response = await auth.api.deviceCode({
143 | 				body: {
144 | 					client_id: "test-client",
145 | 				},
146 | 			});
147 | 
148 | 			expect(response.device_code).toBeDefined();
149 | 			expect(response.user_code).toBeDefined();
150 | 			expect(response.verification_uri).toBeDefined();
151 | 			expect(response.verification_uri_complete).toBeDefined();
152 | 			expect(response.expires_in).toBe(300);
153 | 			expect(response.interval).toBe(2);
154 | 			expect(response.user_code).toMatch(/^[A-Z0-9]{8}$/);
155 | 			expect(response.verification_uri_complete).toContain(response.user_code);
156 | 		});
157 | 
158 | 		it("should support custom client ID and scope", async () => {
159 | 			const response = await auth.api.deviceCode({
160 | 				body: {
161 | 					client_id: "test-client",
162 | 					scope: "read write",
163 | 				},
164 | 			});
165 | 
166 | 			expect(response.device_code).toBeDefined();
167 | 			expect(response.user_code).toBeDefined();
168 | 		});
169 | 	});
170 | 
171 | 	describe("device token polling", () => {
172 | 		it("should return authorization_pending when not approved", async () => {
173 | 			const { device_code } = await auth.api.deviceCode({
174 | 				body: {
175 | 					client_id: "test-client",
176 | 				},
177 | 			});
178 | 
179 | 			await expect(
180 | 				auth.api.deviceToken({
181 | 					body: {
182 | 						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
183 | 						device_code: device_code,
184 | 						client_id: "test-client",
185 | 					},
186 | 				}),
187 | 			).rejects.toMatchObject({
188 | 				body: {
189 | 					error: "authorization_pending",
190 | 					error_description: "Authorization pending",
191 | 				},
192 | 			});
193 | 		});
194 | 
195 | 		it("should return expired_token for expired device codes", async () => {
196 | 			const { device_code } = await auth.api.deviceCode({
197 | 				body: {
198 | 					client_id: "test-client",
199 | 				},
200 | 			});
201 | 
202 | 			// Advance time past expiration
203 | 			vi.useFakeTimers();
204 | 			await vi.advanceTimersByTimeAsync(301 * 1000); // 301 seconds
205 | 
206 | 			await expect(
207 | 				auth.api.deviceToken({
208 | 					body: {
209 | 						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
210 | 						device_code: device_code,
211 | 						client_id: "test-client",
212 | 					},
213 | 				}),
214 | 			).rejects.toMatchObject({
215 | 				body: {
216 | 					error: "expired_token",
217 | 					error_description: "Device code has expired",
218 | 				},
219 | 			});
220 | 
221 | 			vi.useRealTimers();
222 | 		});
223 | 
224 | 		it("should return error for invalid device code", async () => {
225 | 			await expect(
226 | 				auth.api.deviceToken({
227 | 					body: {
228 | 						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
229 | 						device_code: "invalid-code",
230 | 						client_id: "test-client",
231 | 					},
232 | 				}),
233 | 			).rejects.toMatchObject({
234 | 				body: {
235 | 					error: "invalid_grant",
236 | 					error_description: "Invalid device code",
237 | 				},
238 | 			});
239 | 		});
240 | 	});
241 | 
242 | 	describe("device verification", () => {
243 | 		it("should verify valid user code", async () => {
244 | 			const { user_code } = await auth.api.deviceCode({
245 | 				body: {
246 | 					client_id: "test-client",
247 | 				},
248 | 			});
249 | 
250 | 			const response = await auth.api.deviceVerify({
251 | 				query: { user_code },
252 | 			});
253 | 			expect("error" in response).toBe(false);
254 | 			if (!("error" in response)) {
255 | 				expect(response.user_code).toBe(user_code);
256 | 				expect(response.status).toBe("pending");
257 | 			}
258 | 		});
259 | 
260 | 		it("should handle invalid user code", async () => {
261 | 			await expect(
262 | 				auth.api.deviceVerify({
263 | 					query: { user_code: "INVALID" },
264 | 				}),
265 | 			).rejects.toMatchObject({
266 | 				body: {
267 | 					error: "invalid_request",
268 | 					error_description: "Invalid user code",
269 | 				},
270 | 			});
271 | 		});
272 | 	});
273 | 
274 | 	describe("device approval flow", () => {
275 | 		it("should approve device and create session", async () => {
276 | 			// First, sign in as a user
277 | 			const { headers } = await signInWithTestUser();
278 | 
279 | 			// Request device code
280 | 			const { device_code, user_code } = await auth.api.deviceCode({
281 | 				body: {
282 | 					client_id: "test-client",
283 | 				},
284 | 			});
285 | 
286 | 			// Approve the device
287 | 			const approveResponse = await auth.api.deviceApprove({
288 | 				body: { userCode: user_code },
289 | 				headers,
290 | 			});
291 | 			expect("success" in approveResponse && approveResponse.success).toBe(
292 | 				true,
293 | 			);
294 | 
295 | 			// Poll for token should now succeed
296 | 			const tokenResponse = await auth.api.deviceToken({
297 | 				body: {
298 | 					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
299 | 					device_code: device_code,
300 | 					client_id: "test-client",
301 | 				},
302 | 			});
303 | 			// Check OAuth 2.0 compliant response
304 | 			expect("access_token" in tokenResponse).toBe(true);
305 | 			if ("access_token" in tokenResponse) {
306 | 				expect(tokenResponse.access_token).toBeDefined();
307 | 				expect(tokenResponse.token_type).toBe("Bearer");
308 | 				expect(tokenResponse.expires_in).toBeGreaterThan(0);
309 | 				expect(tokenResponse.scope).toBeDefined();
310 | 			}
311 | 		});
312 | 
313 | 		it("should deny device authorization", async () => {
314 | 			const { device_code, user_code } = await auth.api.deviceCode({
315 | 				body: {
316 | 					client_id: "test-client",
317 | 				},
318 | 			});
319 | 
320 | 			// Deny the device
321 | 			const denyResponse = await auth.api.deviceDeny({
322 | 				body: { userCode: user_code },
323 | 				headers: new Headers(),
324 | 			});
325 | 			expect("success" in denyResponse && denyResponse.success).toBe(true);
326 | 
327 | 			// Poll for token should return access_denied
328 | 			await expect(
329 | 				auth.api.deviceToken({
330 | 					body: {
331 | 						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
332 | 						device_code: device_code,
333 | 						client_id: "test-client",
334 | 					},
335 | 				}),
336 | 			).rejects.toMatchObject({
337 | 				body: {
338 | 					error: "access_denied",
339 | 					error_description: "Access denied",
340 | 				},
341 | 			});
342 | 		});
343 | 
344 | 		it("should require authentication for approval", async () => {
345 | 			const { user_code } = await auth.api.deviceCode({
346 | 				body: {
347 | 					client_id: "test-client",
348 | 				},
349 | 			});
350 | 
351 | 			await expect(
352 | 				auth.api.deviceApprove({
353 | 					body: { userCode: user_code },
354 | 					headers: new Headers(),
355 | 				}),
356 | 			).rejects.toMatchObject({
357 | 				body: {
358 | 					error: "unauthorized",
359 | 					error_description: "Authentication required",
360 | 				},
361 | 			});
362 | 		});
363 | 
364 | 		it("should enforce rate limiting with slow_down error", async () => {
365 | 			const { device_code } = await auth.api.deviceCode({
366 | 				body: {
367 | 					client_id: "test-client",
368 | 				},
369 | 			});
370 | 
371 | 			await auth.api
372 | 				.deviceToken({
373 | 					body: {
374 | 						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
375 | 						device_code: device_code,
376 | 						client_id: "test-client",
377 | 					},
378 | 				})
379 | 				.catch(
380 | 					// ignore the error
381 | 					() => {},
382 | 				);
383 | 
384 | 			await expect(
385 | 				auth.api.deviceToken({
386 | 					body: {
387 | 						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
388 | 						device_code: device_code,
389 | 						client_id: "test-client",
390 | 					},
391 | 				}),
392 | 			).rejects.toMatchObject({
393 | 				body: {
394 | 					error: "slow_down",
395 | 					error_description: "Polling too frequently",
396 | 				},
397 | 			});
398 | 		});
399 | 	});
400 | 
401 | 	describe("edge cases", () => {
402 | 		it("should not allow approving already processed device code", async () => {
403 | 			// Sign in as a user
404 | 			const { headers } = await signInWithTestUser();
405 | 
406 | 			// Request and approve device
407 | 			const { user_code: userCode } = await auth.api.deviceCode({
408 | 				body: {
409 | 					client_id: "test-client",
410 | 				},
411 | 			});
412 | 			await auth.api.deviceApprove({
413 | 				body: { userCode },
414 | 				headers,
415 | 			});
416 | 
417 | 			await expect(
418 | 				auth.api.deviceApprove({
419 | 					body: { userCode },
420 | 					headers,
421 | 				}),
422 | 			).rejects.toMatchObject({
423 | 				body: {
424 | 					error: "invalid_request",
425 | 					error_description: "Device code already processed",
426 | 				},
427 | 			});
428 | 		});
429 | 
430 | 		it("should handle user code without dashes", async () => {
431 | 			const { user_code } = await auth.api.deviceCode({
432 | 				body: {
433 | 					client_id: "test-client",
434 | 				},
435 | 			});
436 | 			const cleanUserCode = user_code.replace(/-/g, "");
437 | 
438 | 			const response = await auth.api.deviceVerify({
439 | 				query: { user_code: cleanUserCode },
440 | 			});
441 | 			expect("status" in response && response.status).toBe("pending");
442 | 		});
443 | 
444 | 		it("should store and use scope from device code request", async () => {
445 | 			const { headers } = await signInWithTestUser();
446 | 
447 | 			const { device_code, user_code } = await auth.api.deviceCode({
448 | 				body: {
449 | 					client_id: "test-client",
450 | 					scope: "read write profile",
451 | 				},
452 | 			});
453 | 
454 | 			await auth.api.deviceApprove({
455 | 				body: { userCode: user_code },
456 | 				headers,
457 | 			});
458 | 
459 | 			const tokenResponse = await auth.api.deviceToken({
460 | 				body: {
461 | 					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
462 | 					device_code: device_code,
463 | 					client_id: "test-client",
464 | 				},
465 | 			});
466 | 			expect("scope" in tokenResponse && tokenResponse.scope).toBe(
467 | 				"read write profile",
468 | 			);
469 | 		});
470 | 	});
471 | });
472 | 
473 | describe("device authorization with custom options", async () => {
474 | 	it("should correctly store interval as milliseconds in database", async () => {
475 | 		const { auth, client, db } = await getTestInstance({
476 | 			plugins: [
477 | 				deviceAuthorization({
478 | 					interval: "5s",
479 | 				}),
480 | 			],
481 | 		});
482 | 
483 | 		const response = await auth.api.deviceCode({
484 | 			body: {
485 | 				client_id: "test-client",
486 | 			},
487 | 		});
488 | 
489 | 		// Response should return interval in seconds
490 | 		expect(response.interval).toBe(5);
491 | 
492 | 		// Check that the interval is stored as milliseconds in the database
493 | 		const deviceCodeRecord: DeviceCode | null = await db.findOne({
494 | 			model: "deviceCode",
495 | 			where: [
496 | 				{
497 | 					field: "deviceCode",
498 | 					value: response.device_code,
499 | 				},
500 | 			],
501 | 		});
502 | 
503 | 		// Should be stored as 5000 milliseconds, not "5s" string
504 | 		expect(deviceCodeRecord?.pollingInterval).toBe(5000);
505 | 		expect(typeof deviceCodeRecord?.pollingInterval).toBe("number");
506 | 	});
507 | 
508 | 	it("should use custom code generators", async () => {
509 | 		const customDeviceCode = "custom-device-code-12345";
510 | 		const customUserCode = "CUSTOM12";
511 | 
512 | 		const { auth } = await getTestInstance({
513 | 			plugins: [
514 | 				deviceAuthorization({
515 | 					generateDeviceCode: () => customDeviceCode,
516 | 					generateUserCode: () => customUserCode,
517 | 				}),
518 | 			],
519 | 		});
520 | 
521 | 		const response = await auth.api.deviceCode({
522 | 			body: {
523 | 				client_id: "test-client",
524 | 			},
525 | 		});
526 | 		expect(response.device_code).toBe(customDeviceCode);
527 | 		expect(response.user_code).toBe(customUserCode);
528 | 	});
529 | 
530 | 	it("should respect custom expiration time", async () => {
531 | 		const { auth } = await getTestInstance({
532 | 			plugins: [
533 | 				deviceAuthorization({
534 | 					expiresIn: "1min",
535 | 				}),
536 | 			],
537 | 		});
538 | 
539 | 		const response = await auth.api.deviceCode({
540 | 			body: {
541 | 				client_id: "test-client",
542 | 			},
543 | 		});
544 | 		expect(response.expires_in).toBe(60);
545 | 	});
546 | });
547 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createAuthEndpoint } from "@better-auth/core/api";
  2 | import { APIError } from "better-call";
  3 | import * as z from "zod";
  4 | import { sessionMiddleware } from "../../../api";
  5 | import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
  6 | import { generateRandomString } from "../../../crypto/random";
  7 | import { safeJSONParse } from "../../../utils/json";
  8 | import { TWO_FACTOR_ERROR_CODES } from "../error-code";
  9 | import type {
 10 | 	TwoFactorProvider,
 11 | 	TwoFactorTable,
 12 | 	UserWithTwoFactor,
 13 | } from "../types";
 14 | import { verifyTwoFactor } from "../verify-two-factor";
 15 | 
 16 | export interface BackupCodeOptions {
 17 | 	/**
 18 | 	 * The amount of backup codes to generate
 19 | 	 *
 20 | 	 * @default 10
 21 | 	 */
 22 | 	amount?: number;
 23 | 	/**
 24 | 	 * The length of the backup codes
 25 | 	 *
 26 | 	 * @default 10
 27 | 	 */
 28 | 	length?: number;
 29 | 	/**
 30 | 	 * An optional custom function to generate backup codes
 31 | 	 */
 32 | 	customBackupCodesGenerate?: () => string[];
 33 | 	/**
 34 | 	 * How to store the backup codes in the database, whether encrypted or plain.
 35 | 	 */
 36 | 	storeBackupCodes?:
 37 | 		| "plain"
 38 | 		| "encrypted"
 39 | 		| {
 40 | 				encrypt: (token: string) => Promise<string>;
 41 | 				decrypt: (token: string) => Promise<string>;
 42 | 		  };
 43 | }
 44 | 
 45 | function generateBackupCodesFn(options?: BackupCodeOptions) {
 46 | 	return Array.from({ length: options?.amount ?? 10 })
 47 | 		.fill(null)
 48 | 		.map(() => generateRandomString(options?.length ?? 10, "a-z", "0-9", "A-Z"))
 49 | 		.map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
 50 | }
 51 | 
 52 | export async function generateBackupCodes(
 53 | 	secret: string,
 54 | 	options?: BackupCodeOptions,
 55 | ) {
 56 | 	const backupCodes = options?.customBackupCodesGenerate
 57 | 		? options.customBackupCodesGenerate()
 58 | 		: generateBackupCodesFn(options);
 59 | 	if (options?.storeBackupCodes === "encrypted") {
 60 | 		const encCodes = await symmetricEncrypt({
 61 | 			data: JSON.stringify(backupCodes),
 62 | 			key: secret,
 63 | 		});
 64 | 		return {
 65 | 			backupCodes,
 66 | 			encryptedBackupCodes: encCodes,
 67 | 		};
 68 | 	}
 69 | 	if (
 70 | 		typeof options?.storeBackupCodes === "object" &&
 71 | 		"encrypt" in options?.storeBackupCodes
 72 | 	) {
 73 | 		return {
 74 | 			backupCodes,
 75 | 			encryptedBackupCodes: await options?.storeBackupCodes.encrypt(
 76 | 				JSON.stringify(backupCodes),
 77 | 			),
 78 | 		};
 79 | 	}
 80 | 	return {
 81 | 		backupCodes,
 82 | 		encryptedBackupCodes: JSON.stringify(backupCodes),
 83 | 	};
 84 | }
 85 | 
 86 | export async function verifyBackupCode(
 87 | 	data: {
 88 | 		backupCodes: string;
 89 | 		code: string;
 90 | 	},
 91 | 	key: string,
 92 | 	options?: BackupCodeOptions,
 93 | ) {
 94 | 	const codes = await getBackupCodes(data.backupCodes, key, options);
 95 | 	if (!codes) {
 96 | 		return {
 97 | 			status: false,
 98 | 			updated: null,
 99 | 		};
100 | 	}
101 | 	return {
102 | 		status: codes.includes(data.code),
103 | 		updated: codes.filter((code) => code !== data.code),
104 | 	};
105 | }
106 | 
107 | export async function getBackupCodes(
108 | 	backupCodes: string,
109 | 	key: string,
110 | 	options?: BackupCodeOptions,
111 | ) {
112 | 	if (options?.storeBackupCodes === "encrypted") {
113 | 		const decrypted = await symmetricDecrypt({ key, data: backupCodes });
114 | 		return safeJSONParse<string[]>(decrypted);
115 | 	}
116 | 	if (
117 | 		typeof options?.storeBackupCodes === "object" &&
118 | 		"decrypt" in options?.storeBackupCodes
119 | 	) {
120 | 		const decrypted = await options?.storeBackupCodes.decrypt(backupCodes);
121 | 		return safeJSONParse<string[]>(decrypted);
122 | 	}
123 | 
124 | 	return safeJSONParse<string[]>(backupCodes);
125 | }
126 | 
127 | export const backupCode2fa = (opts: BackupCodeOptions) => {
128 | 	const twoFactorTable = "twoFactor";
129 | 
130 | 	return {
131 | 		id: "backup_code",
132 | 		endpoints: {
133 | 			/**
134 | 			 * ### Endpoint
135 | 			 *
136 | 			 * POST `/two-factor/verify-backup-code`
137 | 			 *
138 | 			 * ### API Methods
139 | 			 *
140 | 			 * **server:**
141 | 			 * `auth.api.verifyBackupCode`
142 | 			 *
143 | 			 * **client:**
144 | 			 * `authClient.twoFactor.verifyBackupCode`
145 | 			 *
146 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-verify-backup-code)
147 | 			 */
148 | 			verifyBackupCode: createAuthEndpoint(
149 | 				"/two-factor/verify-backup-code",
150 | 
151 | 				{
152 | 					method: "POST",
153 | 					body: z.object({
154 | 						code: z.string().meta({
155 | 							description: `A backup code to verify. Eg: "123456"`,
156 | 						}),
157 | 						/**
158 | 						 * Disable setting the session cookie
159 | 						 */
160 | 						disableSession: z
161 | 							.boolean()
162 | 							.meta({
163 | 								description: "If true, the session cookie will not be set.",
164 | 							})
165 | 							.optional(),
166 | 						/**
167 | 						 * if true, the device will be trusted
168 | 						 * for 30 days. It'll be refreshed on
169 | 						 * every sign in request within this time.
170 | 						 */
171 | 						trustDevice: z
172 | 							.boolean()
173 | 							.meta({
174 | 								description:
175 | 									"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
176 | 							})
177 | 							.optional(),
178 | 					}),
179 | 					metadata: {
180 | 						openapi: {
181 | 							description: "Verify a backup code for two-factor authentication",
182 | 							responses: {
183 | 								"200": {
184 | 									description: "Backup code verified successfully",
185 | 									content: {
186 | 										"application/json": {
187 | 											schema: {
188 | 												type: "object",
189 | 												properties: {
190 | 													user: {
191 | 														type: "object",
192 | 														properties: {
193 | 															id: {
194 | 																type: "string",
195 | 																description: "Unique identifier of the user",
196 | 															},
197 | 															email: {
198 | 																type: "string",
199 | 																format: "email",
200 | 																nullable: true,
201 | 																description: "User's email address",
202 | 															},
203 | 															emailVerified: {
204 | 																type: "boolean",
205 | 																nullable: true,
206 | 																description: "Whether the email is verified",
207 | 															},
208 | 															name: {
209 | 																type: "string",
210 | 																nullable: true,
211 | 																description: "User's name",
212 | 															},
213 | 															image: {
214 | 																type: "string",
215 | 																format: "uri",
216 | 																nullable: true,
217 | 																description: "User's profile image URL",
218 | 															},
219 | 															twoFactorEnabled: {
220 | 																type: "boolean",
221 | 																description:
222 | 																	"Whether two-factor authentication is enabled for the user",
223 | 															},
224 | 															createdAt: {
225 | 																type: "string",
226 | 																format: "date-time",
227 | 																description:
228 | 																	"Timestamp when the user was created",
229 | 															},
230 | 															updatedAt: {
231 | 																type: "string",
232 | 																format: "date-time",
233 | 																description:
234 | 																	"Timestamp when the user was last updated",
235 | 															},
236 | 														},
237 | 														required: [
238 | 															"id",
239 | 															"twoFactorEnabled",
240 | 															"createdAt",
241 | 															"updatedAt",
242 | 														],
243 | 														description:
244 | 															"The authenticated user object with two-factor details",
245 | 													},
246 | 													session: {
247 | 														type: "object",
248 | 														properties: {
249 | 															token: {
250 | 																type: "string",
251 | 																description: "Session token",
252 | 															},
253 | 															userId: {
254 | 																type: "string",
255 | 																description:
256 | 																	"ID of the user associated with the session",
257 | 															},
258 | 															createdAt: {
259 | 																type: "string",
260 | 																format: "date-time",
261 | 																description:
262 | 																	"Timestamp when the session was created",
263 | 															},
264 | 															expiresAt: {
265 | 																type: "string",
266 | 																format: "date-time",
267 | 																description:
268 | 																	"Timestamp when the session expires",
269 | 															},
270 | 														},
271 | 														required: [
272 | 															"token",
273 | 															"userId",
274 | 															"createdAt",
275 | 															"expiresAt",
276 | 														],
277 | 														description:
278 | 															"The current session object, included unless disableSession is true",
279 | 													},
280 | 												},
281 | 												required: ["user", "session"],
282 | 											},
283 | 										},
284 | 									},
285 | 								},
286 | 							},
287 | 						},
288 | 					},
289 | 				},
290 | 				async (ctx) => {
291 | 					const { session, valid } = await verifyTwoFactor(ctx);
292 | 					const user = session.user as UserWithTwoFactor;
293 | 					const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({
294 | 						model: twoFactorTable,
295 | 						where: [
296 | 							{
297 | 								field: "userId",
298 | 								value: user.id,
299 | 							},
300 | 						],
301 | 					});
302 | 					if (!twoFactor) {
303 | 						throw new APIError("BAD_REQUEST", {
304 | 							message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
305 | 						});
306 | 					}
307 | 					const validate = await verifyBackupCode(
308 | 						{
309 | 							backupCodes: twoFactor.backupCodes,
310 | 							code: ctx.body.code,
311 | 						},
312 | 						ctx.context.secret,
313 | 						opts,
314 | 					);
315 | 					if (!validate.status) {
316 | 						throw new APIError("UNAUTHORIZED", {
317 | 							message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
318 | 						});
319 | 					}
320 | 					const updatedBackupCodes = await symmetricEncrypt({
321 | 						key: ctx.context.secret,
322 | 						data: JSON.stringify(validate.updated),
323 | 					});
324 | 
325 | 					await ctx.context.adapter.updateMany({
326 | 						model: twoFactorTable,
327 | 						update: {
328 | 							backupCodes: updatedBackupCodes,
329 | 						},
330 | 						where: [
331 | 							{
332 | 								field: "userId",
333 | 								value: user.id,
334 | 							},
335 | 						],
336 | 					});
337 | 
338 | 					if (!ctx.body.disableSession) {
339 | 						return valid(ctx);
340 | 					}
341 | 					return ctx.json({
342 | 						token: session.session?.token,
343 | 						user: {
344 | 							id: session.user?.id,
345 | 							email: session.user.email,
346 | 							emailVerified: session.user.emailVerified,
347 | 							name: session.user.name,
348 | 							image: session.user.image,
349 | 							createdAt: session.user.createdAt,
350 | 							updatedAt: session.user.updatedAt,
351 | 						},
352 | 					});
353 | 				},
354 | 			),
355 | 			/**
356 | 			 * ### Endpoint
357 | 			 *
358 | 			 * POST `/two-factor/generate-backup-codes`
359 | 			 *
360 | 			 * ### API Methods
361 | 			 *
362 | 			 * **server:**
363 | 			 * `auth.api.generateBackupCodes`
364 | 			 *
365 | 			 * **client:**
366 | 			 * `authClient.twoFactor.generateBackupCodes`
367 | 			 *
368 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-generate-backup-codes)
369 | 			 */
370 | 			generateBackupCodes: createAuthEndpoint(
371 | 				"/two-factor/generate-backup-codes",
372 | 				{
373 | 					method: "POST",
374 | 					body: z.object({
375 | 						password: z.string().meta({
376 | 							description: "The users password.",
377 | 						}),
378 | 					}),
379 | 					use: [sessionMiddleware],
380 | 					metadata: {
381 | 						openapi: {
382 | 							description:
383 | 								"Generate new backup codes for two-factor authentication",
384 | 							responses: {
385 | 								"200": {
386 | 									description: "Backup codes generated successfully",
387 | 									content: {
388 | 										"application/json": {
389 | 											schema: {
390 | 												type: "object",
391 | 												properties: {
392 | 													status: {
393 | 														type: "boolean",
394 | 														description:
395 | 															"Indicates if the backup codes were generated successfully",
396 | 														enum: [true],
397 | 													},
398 | 													backupCodes: {
399 | 														type: "array",
400 | 														items: { type: "string" },
401 | 														description:
402 | 															"Array of generated backup codes in plain text",
403 | 													},
404 | 												},
405 | 												required: ["status", "backupCodes"],
406 | 											},
407 | 										},
408 | 									},
409 | 								},
410 | 							},
411 | 						},
412 | 					},
413 | 				},
414 | 				async (ctx) => {
415 | 					const user = ctx.context.session.user as UserWithTwoFactor;
416 | 					if (!user.twoFactorEnabled) {
417 | 						throw new APIError("BAD_REQUEST", {
418 | 							message: TWO_FACTOR_ERROR_CODES.TWO_FACTOR_NOT_ENABLED,
419 | 						});
420 | 					}
421 | 					await ctx.context.password.checkPassword(user.id, ctx);
422 | 					const backupCodes = await generateBackupCodes(
423 | 						ctx.context.secret,
424 | 						opts,
425 | 					);
426 | 					await ctx.context.adapter.updateMany({
427 | 						model: twoFactorTable,
428 | 						update: {
429 | 							backupCodes: backupCodes.encryptedBackupCodes,
430 | 						},
431 | 						where: [
432 | 							{
433 | 								field: "userId",
434 | 								value: ctx.context.session.user.id,
435 | 							},
436 | 						],
437 | 					});
438 | 					return ctx.json({
439 | 						status: true,
440 | 						backupCodes: backupCodes.backupCodes,
441 | 					});
442 | 				},
443 | 			),
444 | 			/**
445 | 			 * ### Endpoint
446 | 			 *
447 | 			 * GET `/two-factor/view-backup-codes`
448 | 			 *
449 | 			 * ### API Methods
450 | 			 *
451 | 			 * **server:**
452 | 			 * `auth.api.viewBackupCodes`
453 | 			 *
454 | 			 * **client:**
455 | 			 * `authClient.twoFactor.viewBackupCodes`
456 | 			 *
457 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-view-backup-codes)
458 | 			 */
459 | 			viewBackupCodes: createAuthEndpoint(
460 | 				"/two-factor/view-backup-codes",
461 | 				{
462 | 					method: "GET",
463 | 					body: z.object({
464 | 						userId: z.coerce.string().meta({
465 | 							description: `The user ID to view all backup codes. Eg: "user-id"`,
466 | 						}),
467 | 					}),
468 | 					metadata: {
469 | 						SERVER_ONLY: true,
470 | 					},
471 | 				},
472 | 				async (ctx) => {
473 | 					const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({
474 | 						model: twoFactorTable,
475 | 						where: [
476 | 							{
477 | 								field: "userId",
478 | 								value: ctx.body.userId,
479 | 							},
480 | 						],
481 | 					});
482 | 					if (!twoFactor) {
483 | 						throw new APIError("BAD_REQUEST", {
484 | 							message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
485 | 						});
486 | 					}
487 | 					const decryptedBackupCodes = await getBackupCodes(
488 | 						twoFactor.backupCodes,
489 | 						ctx.context.secret,
490 | 						opts,
491 | 					);
492 | 
493 | 					if (!decryptedBackupCodes) {
494 | 						throw new APIError("BAD_REQUEST", {
495 | 							message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
496 | 						});
497 | 					}
498 | 					return ctx.json({
499 | 						status: true,
500 | 						backupCodes: decryptedBackupCodes,
501 | 					});
502 | 				},
503 | 			),
504 | 		},
505 | 	} satisfies TwoFactorProvider;
506 | };
507 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/drizzle-adapter/drizzle-adapter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { BetterAuthOptions } from "@better-auth/core";
  2 | import type {
  3 | 	DBAdapter,
  4 | 	DBAdapterDebugLogOption,
  5 | 	Where,
  6 | } from "@better-auth/core/db/adapter";
  7 | import { BetterAuthError } from "@better-auth/core/error";
  8 | import {
  9 | 	and,
 10 | 	asc,
 11 | 	count,
 12 | 	desc,
 13 | 	eq,
 14 | 	gt,
 15 | 	gte,
 16 | 	inArray,
 17 | 	like,
 18 | 	lt,
 19 | 	lte,
 20 | 	ne,
 21 | 	notInArray,
 22 | 	or,
 23 | 	SQL,
 24 | 	sql,
 25 | } from "drizzle-orm";
 26 | import {
 27 | 	type AdapterFactoryCustomizeAdapterCreator,
 28 | 	type AdapterFactoryOptions,
 29 | 	createAdapterFactory,
 30 | } from "../adapter-factory";
 31 | 
 32 | export interface DB {
 33 | 	[key: string]: any;
 34 | }
 35 | 
 36 | export interface DrizzleAdapterConfig {
 37 | 	/**
 38 | 	 * The schema object that defines the tables and fields
 39 | 	 */
 40 | 	schema?: Record<string, any>;
 41 | 	/**
 42 | 	 * The database provider
 43 | 	 */
 44 | 	provider: "pg" | "mysql" | "sqlite";
 45 | 	/**
 46 | 	 * If the table names in the schema are plural
 47 | 	 * set this to true. For example, if the schema
 48 | 	 * has an object with a key "users" instead of "user"
 49 | 	 */
 50 | 	usePlural?: boolean;
 51 | 	/**
 52 | 	 * Enable debug logs for the adapter
 53 | 	 *
 54 | 	 * @default false
 55 | 	 */
 56 | 	debugLogs?: DBAdapterDebugLogOption;
 57 | 	/**
 58 | 	 * By default snake case is used for table and field names
 59 | 	 * when the CLI is used to generate the schema. If you want
 60 | 	 * to use camel case, set this to true.
 61 | 	 * @default false
 62 | 	 */
 63 | 	camelCase?: boolean;
 64 | 	/**
 65 | 	 * Whether to execute multiple operations in a transaction.
 66 | 	 *
 67 | 	 * If the database doesn't support transactions,
 68 | 	 * set this to `false` and operations will be executed sequentially.
 69 | 	 * @default false
 70 | 	 */
 71 | 	transaction?: boolean;
 72 | }
 73 | 
 74 | export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
 75 | 	let lazyOptions: BetterAuthOptions | null = null;
 76 | 	const createCustomAdapter =
 77 | 		(db: DB): AdapterFactoryCustomizeAdapterCreator =>
 78 | 		({ getFieldName, debugLog }) => {
 79 | 			function getSchema(model: string) {
 80 | 				const schema = config.schema || db._.fullSchema;
 81 | 				if (!schema) {
 82 | 					throw new BetterAuthError(
 83 | 						"Drizzle adapter failed to initialize. Schema not found. Please provide a schema object in the adapter options object.",
 84 | 					);
 85 | 				}
 86 | 				const schemaModel = schema[model];
 87 | 				if (!schemaModel) {
 88 | 					throw new BetterAuthError(
 89 | 						`[# Drizzle Adapter]: The model "${model}" was not found in the schema object. Please pass the schema directly to the adapter options.`,
 90 | 					);
 91 | 				}
 92 | 				return schemaModel;
 93 | 			}
 94 | 			const withReturning = async (
 95 | 				model: string,
 96 | 				builder: any,
 97 | 				data: Record<string, any>,
 98 | 				where?: Where[],
 99 | 			) => {
100 | 				if (config.provider !== "mysql") {
101 | 					const c = await builder.returning();
102 | 					return c[0];
103 | 				}
104 | 				await builder.execute();
105 | 				const schemaModel = getSchema(model);
106 | 				const builderVal = builder.config?.values;
107 | 				if (where?.length) {
108 | 					// If we're updating a field that's in the where clause, use the new value
109 | 					const updatedWhere = where.map((w) => {
110 | 						// If this field was updated, use the new value for lookup
111 | 						if (data[w.field] !== undefined) {
112 | 							return { ...w, value: data[w.field] };
113 | 						}
114 | 						return w;
115 | 					});
116 | 
117 | 					const clause = convertWhereClause(updatedWhere, model);
118 | 					const res = await db
119 | 						.select()
120 | 						.from(schemaModel)
121 | 						.where(...clause);
122 | 					return res[0];
123 | 				} else if (builderVal && builderVal[0]?.id?.value) {
124 | 					let tId = builderVal[0]?.id?.value;
125 | 					if (!tId) {
126 | 						//get last inserted id
127 | 						const lastInsertId = await db
128 | 							.select({ id: sql`LAST_INSERT_ID()` })
129 | 							.from(schemaModel)
130 | 							.orderBy(desc(schemaModel.id))
131 | 							.limit(1);
132 | 						tId = lastInsertId[0].id;
133 | 					}
134 | 					const res = await db
135 | 						.select()
136 | 						.from(schemaModel)
137 | 						.where(eq(schemaModel.id, tId))
138 | 						.limit(1)
139 | 						.execute();
140 | 					return res[0];
141 | 				} else if (data.id) {
142 | 					const res = await db
143 | 						.select()
144 | 						.from(schemaModel)
145 | 						.where(eq(schemaModel.id, data.id))
146 | 						.limit(1)
147 | 						.execute();
148 | 					return res[0];
149 | 				} else {
150 | 					// If the user doesn't have `id` as a field, then this will fail.
151 | 					// We expect that they defined `id` in all of their models.
152 | 					if (!("id" in schemaModel)) {
153 | 						throw new BetterAuthError(
154 | 							`The model "${model}" does not have an "id" field. Please use the "id" field as your primary key.`,
155 | 						);
156 | 					}
157 | 					const res = await db
158 | 						.select()
159 | 						.from(schemaModel)
160 | 						.orderBy(desc(schemaModel.id))
161 | 						.limit(1)
162 | 						.execute();
163 | 					return res[0];
164 | 				}
165 | 			};
166 | 			function convertWhereClause(where: Where[], model: string) {
167 | 				const schemaModel = getSchema(model);
168 | 				if (!where) return [];
169 | 				if (where.length === 1) {
170 | 					const w = where[0];
171 | 					if (!w) {
172 | 						return [];
173 | 					}
174 | 					const field = getFieldName({ model, field: w.field });
175 | 					if (!schemaModel[field]) {
176 | 						throw new BetterAuthError(
177 | 							`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`,
178 | 						);
179 | 					}
180 | 					if (w.operator === "in") {
181 | 						if (!Array.isArray(w.value)) {
182 | 							throw new BetterAuthError(
183 | 								`The value for the field "${w.field}" must be an array when using the "in" operator.`,
184 | 							);
185 | 						}
186 | 						return [inArray(schemaModel[field], w.value)];
187 | 					}
188 | 
189 | 					if (w.operator === "not_in") {
190 | 						if (!Array.isArray(w.value)) {
191 | 							throw new BetterAuthError(
192 | 								`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
193 | 							);
194 | 						}
195 | 						return [notInArray(schemaModel[field], w.value)];
196 | 					}
197 | 
198 | 					if (w.operator === "contains") {
199 | 						return [like(schemaModel[field], `%${w.value}%`)];
200 | 					}
201 | 
202 | 					if (w.operator === "starts_with") {
203 | 						return [like(schemaModel[field], `${w.value}%`)];
204 | 					}
205 | 
206 | 					if (w.operator === "ends_with") {
207 | 						return [like(schemaModel[field], `%${w.value}`)];
208 | 					}
209 | 
210 | 					if (w.operator === "lt") {
211 | 						return [lt(schemaModel[field], w.value)];
212 | 					}
213 | 
214 | 					if (w.operator === "lte") {
215 | 						return [lte(schemaModel[field], w.value)];
216 | 					}
217 | 
218 | 					if (w.operator === "ne") {
219 | 						return [ne(schemaModel[field], w.value)];
220 | 					}
221 | 
222 | 					if (w.operator === "gt") {
223 | 						return [gt(schemaModel[field], w.value)];
224 | 					}
225 | 
226 | 					if (w.operator === "gte") {
227 | 						return [gte(schemaModel[field], w.value)];
228 | 					}
229 | 
230 | 					return [eq(schemaModel[field], w.value)];
231 | 				}
232 | 				const andGroup = where.filter(
233 | 					(w) => w.connector === "AND" || !w.connector,
234 | 				);
235 | 				const orGroup = where.filter((w) => w.connector === "OR");
236 | 
237 | 				const andClause = and(
238 | 					...andGroup.map((w) => {
239 | 						const field = getFieldName({ model, field: w.field });
240 | 						if (w.operator === "in") {
241 | 							if (!Array.isArray(w.value)) {
242 | 								throw new BetterAuthError(
243 | 									`The value for the field "${w.field}" must be an array when using the "in" operator.`,
244 | 								);
245 | 							}
246 | 							return inArray(schemaModel[field], w.value);
247 | 						}
248 | 						if (w.operator === "not_in") {
249 | 							if (!Array.isArray(w.value)) {
250 | 								throw new BetterAuthError(
251 | 									`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
252 | 								);
253 | 							}
254 | 							return notInArray(schemaModel[field], w.value);
255 | 						}
256 | 						if (w.operator === "contains") {
257 | 							return like(schemaModel[field], `%${w.value}%`);
258 | 						}
259 | 						if (w.operator === "starts_with") {
260 | 							return like(schemaModel[field], `${w.value}%`);
261 | 						}
262 | 						if (w.operator === "ends_with") {
263 | 							return like(schemaModel[field], `%${w.value}`);
264 | 						}
265 | 						if (w.operator === "lt") {
266 | 							return lt(schemaModel[field], w.value);
267 | 						}
268 | 						if (w.operator === "lte") {
269 | 							return lte(schemaModel[field], w.value);
270 | 						}
271 | 						if (w.operator === "gt") {
272 | 							return gt(schemaModel[field], w.value);
273 | 						}
274 | 						if (w.operator === "gte") {
275 | 							return gte(schemaModel[field], w.value);
276 | 						}
277 | 						if (w.operator === "ne") {
278 | 							return ne(schemaModel[field], w.value);
279 | 						}
280 | 						return eq(schemaModel[field], w.value);
281 | 					}),
282 | 				);
283 | 				const orClause = or(
284 | 					...orGroup.map((w) => {
285 | 						const field = getFieldName({ model, field: w.field });
286 | 						if (w.operator === "in") {
287 | 							if (!Array.isArray(w.value)) {
288 | 								throw new BetterAuthError(
289 | 									`The value for the field "${w.field}" must be an array when using the "in" operator.`,
290 | 								);
291 | 							}
292 | 							return inArray(schemaModel[field], w.value);
293 | 						}
294 | 						if (w.operator === "not_in") {
295 | 							if (!Array.isArray(w.value)) {
296 | 								throw new BetterAuthError(
297 | 									`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
298 | 								);
299 | 							}
300 | 							return notInArray(schemaModel[field], w.value);
301 | 						}
302 | 						if (w.operator === "contains") {
303 | 							return like(schemaModel[field], `%${w.value}%`);
304 | 						}
305 | 						if (w.operator === "starts_with") {
306 | 							return like(schemaModel[field], `${w.value}%`);
307 | 						}
308 | 						if (w.operator === "ends_with") {
309 | 							return like(schemaModel[field], `%${w.value}`);
310 | 						}
311 | 						if (w.operator === "lt") {
312 | 							return lt(schemaModel[field], w.value);
313 | 						}
314 | 						if (w.operator === "lte") {
315 | 							return lte(schemaModel[field], w.value);
316 | 						}
317 | 						if (w.operator === "gt") {
318 | 							return gt(schemaModel[field], w.value);
319 | 						}
320 | 						if (w.operator === "gte") {
321 | 							return gte(schemaModel[field], w.value);
322 | 						}
323 | 						if (w.operator === "ne") {
324 | 							return ne(schemaModel[field], w.value);
325 | 						}
326 | 						return eq(schemaModel[field], w.value);
327 | 					}),
328 | 				);
329 | 
330 | 				const clause: SQL<unknown>[] = [];
331 | 
332 | 				if (andGroup.length) clause.push(andClause!);
333 | 				if (orGroup.length) clause.push(orClause!);
334 | 				return clause;
335 | 			}
336 | 			function checkMissingFields(
337 | 				schema: Record<string, any>,
338 | 				model: string,
339 | 				values: Record<string, any>,
340 | 			) {
341 | 				if (!schema) {
342 | 					throw new BetterAuthError(
343 | 						"Drizzle adapter failed to initialize. Schema not found. Please provide a schema object in the adapter options object.",
344 | 					);
345 | 				}
346 | 				for (const key in values) {
347 | 					if (!schema[key]) {
348 | 						throw new BetterAuthError(
349 | 							`The field "${key}" does not exist in the "${model}" schema. Please update your drizzle schema or re-generate using "npx @better-auth/cli generate".`,
350 | 						);
351 | 					}
352 | 				}
353 | 			}
354 | 			return {
355 | 				async create({ model, data: values }) {
356 | 					const schemaModel = getSchema(model);
357 | 					checkMissingFields(schemaModel, model, values);
358 | 					const builder = db.insert(schemaModel).values(values);
359 | 					const returned = await withReturning(model, builder, values);
360 | 					return returned;
361 | 				},
362 | 				async findOne({ model, where }) {
363 | 					const schemaModel = getSchema(model);
364 | 					const clause = convertWhereClause(where, model);
365 | 					const res = await db
366 | 						.select()
367 | 						.from(schemaModel)
368 | 						.where(...clause);
369 | 					if (!res.length) return null;
370 | 					return res[0];
371 | 				},
372 | 				async findMany({ model, where, sortBy, limit, offset }) {
373 | 					const schemaModel = getSchema(model);
374 | 					const clause = where ? convertWhereClause(where, model) : [];
375 | 
376 | 					const sortFn = sortBy?.direction === "desc" ? desc : asc;
377 | 					const builder = db
378 | 						.select()
379 | 						.from(schemaModel)
380 | 						.limit(limit || 100)
381 | 						.offset(offset || 0);
382 | 					if (sortBy?.field) {
383 | 						builder.orderBy(
384 | 							sortFn(
385 | 								schemaModel[getFieldName({ model, field: sortBy?.field })],
386 | 							),
387 | 						);
388 | 					}
389 | 					return (await builder.where(...clause)) as any[];
390 | 				},
391 | 				async count({ model, where }) {
392 | 					const schemaModel = getSchema(model);
393 | 					const clause = where ? convertWhereClause(where, model) : [];
394 | 					const res = await db
395 | 						.select({ count: count() })
396 | 						.from(schemaModel)
397 | 						.where(...clause);
398 | 					return res[0].count;
399 | 				},
400 | 				async update({ model, where, update: values }) {
401 | 					const schemaModel = getSchema(model);
402 | 					const clause = convertWhereClause(where, model);
403 | 					const builder = db
404 | 						.update(schemaModel)
405 | 						.set(values)
406 | 						.where(...clause);
407 | 					return await withReturning(model, builder, values as any, where);
408 | 				},
409 | 				async updateMany({ model, where, update: values }) {
410 | 					const schemaModel = getSchema(model);
411 | 					const clause = convertWhereClause(where, model);
412 | 					const builder = db
413 | 						.update(schemaModel)
414 | 						.set(values)
415 | 						.where(...clause);
416 | 					return await builder;
417 | 				},
418 | 				async delete({ model, where }) {
419 | 					const schemaModel = getSchema(model);
420 | 					const clause = convertWhereClause(where, model);
421 | 					const builder = db.delete(schemaModel).where(...clause);
422 | 					return await builder;
423 | 				},
424 | 				async deleteMany({ model, where }) {
425 | 					const schemaModel = getSchema(model);
426 | 					const clause = convertWhereClause(where, model);
427 | 					const builder = db.delete(schemaModel).where(...clause);
428 | 					return await builder;
429 | 				},
430 | 				options: config,
431 | 			};
432 | 		};
433 | 	let adapterOptions: AdapterFactoryOptions | null = null;
434 | 	adapterOptions = {
435 | 		config: {
436 | 			adapterId: "drizzle",
437 | 			adapterName: "Drizzle Adapter",
438 | 			usePlural: config.usePlural ?? false,
439 | 			debugLogs: config.debugLogs ?? false,
440 | 			transaction:
441 | 				(config.transaction ?? false)
442 | 					? (cb) =>
443 | 							db.transaction((tx: DB) => {
444 | 								const adapter = createAdapterFactory({
445 | 									config: adapterOptions!.config,
446 | 									adapter: createCustomAdapter(tx),
447 | 								})(lazyOptions!);
448 | 								return cb(adapter);
449 | 							})
450 | 					: false,
451 | 		},
452 | 		adapter: createCustomAdapter(db),
453 | 	};
454 | 	const adapter = createAdapterFactory(adapterOptions);
455 | 	return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
456 | 		lazyOptions = options;
457 | 		return adapter(options);
458 | 	};
459 | };
460 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/middlewares/origin-check.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createAuthEndpoint } from "@better-auth/core/api";
  2 | import { describe, expect } from "vitest";
  3 | import * as z from "zod";
  4 | import { createAuthClient } from "../../client";
  5 | import { getTestInstance } from "../../test-utils/test-instance";
  6 | import { isSimpleRequest, originCheck } from "./origin-check";
  7 | 
  8 | describe("Origin Check", async (it) => {
  9 | 	const { customFetchImpl, testUser } = await getTestInstance({
 10 | 		trustedOrigins: [
 11 | 			"http://localhost:5000",
 12 | 			"https://trusted.com",
 13 | 			"*.my-site.com",
 14 | 			"https://*.protocol-site.com",
 15 | 		],
 16 | 		emailAndPassword: {
 17 | 			enabled: true,
 18 | 			async sendResetPassword(url, user) {},
 19 | 		},
 20 | 		advanced: {
 21 | 			disableCSRFCheck: false,
 22 | 			disableOriginCheck: false,
 23 | 		},
 24 | 	});
 25 | 
 26 | 	it("should allow trusted origins", async (ctx) => {
 27 | 		const client = createAuthClient({
 28 | 			baseURL: "http://localhost:3000",
 29 | 			fetchOptions: {
 30 | 				customFetchImpl,
 31 | 				headers: {
 32 | 					origin: "http://localhost:3000",
 33 | 				},
 34 | 			},
 35 | 		});
 36 | 		const res = await client.signIn.email({
 37 | 			email: testUser.email,
 38 | 			password: testUser.password,
 39 | 			callbackURL: "http://localhost:3000/callback",
 40 | 		});
 41 | 		expect(res.data?.user).toBeDefined();
 42 | 	});
 43 | 
 44 | 	it("should not allow untrusted origins", async (ctx) => {
 45 | 		const client = createAuthClient({
 46 | 			baseURL: "http://localhost:3000",
 47 | 			fetchOptions: {
 48 | 				customFetchImpl,
 49 | 			},
 50 | 		});
 51 | 		const res = await client.signIn.email({
 52 | 			email: "[email protected]",
 53 | 			password: "password",
 54 | 			callbackURL: "http://malicious.com",
 55 | 		});
 56 | 		expect(res.error?.status).toBe(403);
 57 | 		expect(res.error?.message).toBe("Invalid callbackURL");
 58 | 	});
 59 | 
 60 | 	it("should allow query params in callback url", async (ctx) => {
 61 | 		const client = createAuthClient({
 62 | 			baseURL: "http://localhost:3000",
 63 | 			fetchOptions: {
 64 | 				customFetchImpl,
 65 | 				headers: {
 66 | 					origin: "https://localhost:3000",
 67 | 				},
 68 | 			},
 69 | 		});
 70 | 		const res = await client.signIn.email({
 71 | 			email: testUser.email,
 72 | 			password: testUser.password,
 73 | 			callbackURL: "/dashboard?test=123",
 74 | 		});
 75 | 		expect(res.data?.user).toBeDefined();
 76 | 	});
 77 | 
 78 | 	it("should allow plus signs in the callback url", async (ctx) => {
 79 | 		const client = createAuthClient({
 80 | 			baseURL: "http://localhost:3000",
 81 | 			fetchOptions: {
 82 | 				customFetchImpl,
 83 | 				headers: {
 84 | 					origin: "https://localhost:3000",
 85 | 				},
 86 | 			},
 87 | 		});
 88 | 		const res = await client.signIn.email({
 89 | 			email: testUser.email,
 90 | 			password: testUser.password,
 91 | 			callbackURL: "/dashboard+page?test=123+456",
 92 | 		});
 93 | 		expect(res.data?.user).toBeDefined();
 94 | 	});
 95 | 
 96 | 	it("should reject callback url with double slash", async (ctx) => {
 97 | 		const client = createAuthClient({
 98 | 			baseURL: "http://localhost:3000",
 99 | 			fetchOptions: {
100 | 				customFetchImpl,
101 | 				headers: {
102 | 					origin: "https://localhost:3000",
103 | 				},
104 | 			},
105 | 		});
106 | 		const res = await client.signIn.email({
107 | 			email: testUser.email,
108 | 			password: testUser.password,
109 | 			callbackURL: "//evil.com",
110 | 		});
111 | 		expect(res.error?.status).toBe(403);
112 | 	});
113 | 
114 | 	it("should reject callback urls with encoded malicious content", async (ctx) => {
115 | 		const client = createAuthClient({
116 | 			baseURL: "http://localhost:3000",
117 | 			fetchOptions: {
118 | 				customFetchImpl,
119 | 				headers: {
120 | 					origin: "https://localhost:3000",
121 | 				},
122 | 			},
123 | 		});
124 | 
125 | 		const maliciousPatterns = [
126 | 			"/%5C/evil.com",
127 | 			`/\\/\\/evil.com`,
128 | 			"/%5C/evil.com",
129 | 			"/..%2F..%2Fevil.com",
130 | 			"javascript:alert('xss')",
131 | 			"data:text/html,<script>alert('xss')</script>",
132 | 		];
133 | 
134 | 		for (const pattern of maliciousPatterns) {
135 | 			const res = await client.signIn.email({
136 | 				email: testUser.email,
137 | 				password: testUser.password,
138 | 				callbackURL: pattern,
139 | 			});
140 | 			expect(res.error?.status).toBe(403);
141 | 		}
142 | 	});
143 | 
144 | 	it("should reject untrusted origin headers", async (ctx) => {
145 | 		const client = createAuthClient({
146 | 			baseURL: "http://localhost:3000",
147 | 			fetchOptions: {
148 | 				customFetchImpl,
149 | 				headers: {
150 | 					origin: "malicious.com",
151 | 					cookie: "session=123",
152 | 				},
153 | 			},
154 | 		});
155 | 		const res = await client.signIn.email({
156 | 			email: testUser.email,
157 | 			password: testUser.password,
158 | 		});
159 | 		expect(res.error?.status).toBe(403);
160 | 	});
161 | 
162 | 	it("should reject untrusted origin headers which start with trusted origin", async (ctx) => {
163 | 		const client = createAuthClient({
164 | 			baseURL: "http://localhost:3000",
165 | 			fetchOptions: {
166 | 				customFetchImpl,
167 | 				headers: {
168 | 					origin: "https://trusted.com.malicious.com",
169 | 					cookie: "session=123",
170 | 				},
171 | 			},
172 | 		});
173 | 		const res = await client.signIn.email({
174 | 			email: testUser.email,
175 | 			password: testUser.password,
176 | 		});
177 | 		expect(res.error?.status).toBe(403);
178 | 	});
179 | 
180 | 	it("should reject untrusted origin subdomains", async (ctx) => {
181 | 		const client = createAuthClient({
182 | 			baseURL: "http://localhost:3000",
183 | 			fetchOptions: {
184 | 				customFetchImpl,
185 | 				headers: {
186 | 					origin: "http://sub-domain.trusted.com",
187 | 					cookie: "session=123",
188 | 				},
189 | 			},
190 | 		});
191 | 		const res = await client.signIn.email({
192 | 			email: testUser.email,
193 | 			password: testUser.password,
194 | 		});
195 | 		expect(res.error?.status).toBe(403);
196 | 	});
197 | 
198 | 	it("should allow untrusted origin if they don't contain cookies", async (ctx) => {
199 | 		const client = createAuthClient({
200 | 			baseURL: "http://localhost:3000",
201 | 			fetchOptions: {
202 | 				customFetchImpl,
203 | 				headers: {
204 | 					origin: "http://sub-domain.trusted.com",
205 | 				},
206 | 			},
207 | 		});
208 | 		const res = await client.signIn.email({
209 | 			email: testUser.email,
210 | 			password: testUser.password,
211 | 		});
212 | 		expect(res.data?.user).toBeDefined();
213 | 	});
214 | 
215 | 	it("should reject untrusted redirectTo", async (ctx) => {
216 | 		const client = createAuthClient({
217 | 			baseURL: "http://localhost:3000",
218 | 			fetchOptions: {
219 | 				customFetchImpl,
220 | 			},
221 | 		});
222 | 		const res = await client.requestPasswordReset({
223 | 			email: testUser.email,
224 | 			redirectTo: "http://malicious.com",
225 | 		});
226 | 		expect(res.error?.status).toBe(403);
227 | 		expect(res.error?.message).toBe("Invalid redirectURL");
228 | 	});
229 | 
230 | 	it("should work with list of trusted origins", async (ctx) => {
231 | 		const client = createAuthClient({
232 | 			baseURL: "http://localhost:3000",
233 | 			fetchOptions: {
234 | 				customFetchImpl,
235 | 				headers: {
236 | 					origin: "https://trusted.com",
237 | 				},
238 | 			},
239 | 		});
240 | 		const res = await client.requestPasswordReset({
241 | 			email: testUser.email,
242 | 			redirectTo: "http://localhost:5000/reset-password",
243 | 		});
244 | 		expect(res.data?.status).toBeTruthy();
245 | 
246 | 		const res2 = await client.signIn.email({
247 | 			email: testUser.email,
248 | 			password: testUser.password,
249 | 			fetchOptions: {
250 | 				query: {
251 | 					currentURL: "http://localhost:5000",
252 | 				},
253 | 			},
254 | 		});
255 | 		expect(res2.data?.user).toBeDefined();
256 | 	});
257 | 
258 | 	it("should work with wildcard trusted origins", async (ctx) => {
259 | 		const client = createAuthClient({
260 | 			baseURL: "https://sub-domain.my-site.com",
261 | 			fetchOptions: {
262 | 				customFetchImpl,
263 | 				headers: {
264 | 					origin: "https://sub-domain.my-site.com",
265 | 				},
266 | 			},
267 | 		});
268 | 		const res = await client.signIn.email({
269 | 			email: testUser.email,
270 | 			password: testUser.password,
271 | 			callbackURL: "https://sub-domain.my-site.com/callback",
272 | 		});
273 | 		expect(res.data?.user).toBeDefined();
274 | 
275 | 		// Test another subdomain with the wildcard pattern
276 | 		const client2 = createAuthClient({
277 | 			baseURL: "https://another-sub.my-site.com",
278 | 			fetchOptions: {
279 | 				customFetchImpl,
280 | 				headers: {
281 | 					origin: "https://another-sub.my-site.com",
282 | 				},
283 | 			},
284 | 		});
285 | 		const res2 = await client2.signIn.email({
286 | 			email: testUser.email,
287 | 			password: testUser.password,
288 | 			callbackURL: "https://another-sub.my-site.com/callback",
289 | 		});
290 | 		expect(res2.data?.user).toBeDefined();
291 | 	});
292 | 
293 | 	it("should work with GET requests", async (ctx) => {
294 | 		const client = createAuthClient({
295 | 			baseURL: "https://sub-domain.my-site.com",
296 | 			fetchOptions: {
297 | 				customFetchImpl,
298 | 				headers: {
299 | 					origin: "https://google.com",
300 | 					cookie: "value",
301 | 				},
302 | 			},
303 | 		});
304 | 		const res = await client.$fetch("/ok");
305 | 		expect(res.data).toMatchObject({ ok: true });
306 | 	});
307 | 
308 | 	it("should handle POST requests with proper origin validation", async (ctx) => {
309 | 		// Test with valid origin
310 | 		const validClient = createAuthClient({
311 | 			baseURL: "http://localhost:3000",
312 | 			fetchOptions: {
313 | 				customFetchImpl,
314 | 				headers: {
315 | 					origin: "http://localhost:5000",
316 | 					cookie: "session=123",
317 | 				},
318 | 			},
319 | 		});
320 | 		const validRes = await validClient.signIn.email({
321 | 			email: testUser.email,
322 | 			password: testUser.password,
323 | 		});
324 | 		expect(validRes.data?.user).toBeDefined();
325 | 
326 | 		// Test with invalid origin
327 | 		const invalidClient = createAuthClient({
328 | 			baseURL: "http://localhost:3000",
329 | 			fetchOptions: {
330 | 				customFetchImpl,
331 | 				headers: {
332 | 					origin: "http://untrusted-domain.com",
333 | 					cookie: "session=123",
334 | 				},
335 | 			},
336 | 		});
337 | 		const invalidRes = await invalidClient.signIn.email({
338 | 			email: testUser.email,
339 | 			password: testUser.password,
340 | 		});
341 | 		expect(invalidRes.error?.status).toBe(403);
342 | 	});
343 | 
344 | 	it("should work with relative callbackURL with query params", async (ctx) => {
345 | 		const client = createAuthClient({
346 | 			baseURL: "http://localhost:3000",
347 | 			fetchOptions: {
348 | 				customFetchImpl,
349 | 			},
350 | 		});
351 | 		const res = await client.signIn.email({
352 | 			email: testUser.email,
353 | 			password: testUser.password,
354 | 			callbackURL: "/[email protected]",
355 | 		});
356 | 		expect(res.data?.user).toBeDefined();
357 | 	});
358 | 
359 | 	it("should work with protocol specific wildcard trusted origins", async () => {
360 | 		// Test HTTPS protocol specific wildcard - should work
361 | 		const httpsClient = createAuthClient({
362 | 			baseURL: "http://localhost:3000",
363 | 			fetchOptions: {
364 | 				customFetchImpl,
365 | 				headers: {
366 | 					origin: "https://api.protocol-site.com",
367 | 					cookie: "session=123",
368 | 				},
369 | 			},
370 | 		});
371 | 		const httpsRes = await httpsClient.signIn.email({
372 | 			email: testUser.email,
373 | 			password: testUser.password,
374 | 			callbackURL: "https://app.protocol-site.com/dashboard",
375 | 		});
376 | 		expect(httpsRes.data?.user).toBeDefined();
377 | 
378 | 		// Test HTTP with HTTPS protocol wildcard - should fail
379 | 		const httpClient = createAuthClient({
380 | 			baseURL: "http://localhost:3000",
381 | 			fetchOptions: {
382 | 				customFetchImpl,
383 | 				headers: {
384 | 					origin: "http://api.protocol-site.com",
385 | 					cookie: "session=123",
386 | 				},
387 | 			},
388 | 		});
389 | 		const httpRes = await httpClient.signIn.email({
390 | 			email: testUser.email,
391 | 			password: testUser.password,
392 | 		});
393 | 		expect(httpRes.error?.status).toBe(403);
394 | 	});
395 | 
396 | 	it("should work with custom scheme wildcards (e.g. exp:// for Expo)", async () => {
397 | 		const { customFetchImpl, testUser } = await getTestInstance({
398 | 			trustedOrigins: [
399 | 				"exp://10.0.0.*:*/*",
400 | 				"exp://192.168.*.*:*/*",
401 | 				"exp://172.*.*.*:*/*",
402 | 			],
403 | 			emailAndPassword: {
404 | 				enabled: true,
405 | 				async sendResetPassword(url, user) {},
406 | 			},
407 | 		});
408 | 
409 | 		// Test custom scheme with wildcard - should work
410 | 		const expoClient = createAuthClient({
411 | 			baseURL: "http://localhost:3000",
412 | 			fetchOptions: {
413 | 				customFetchImpl,
414 | 			},
415 | 		});
416 | 
417 | 		// Test with IP matching the wildcard pattern
418 | 		const resWithIP = await expoClient.signIn.email({
419 | 			email: testUser.email,
420 | 			password: testUser.password,
421 | 			callbackURL: "exp://10.0.0.29:8081/--/",
422 | 		});
423 | 		expect(resWithIP.data?.user).toBeDefined();
424 | 
425 | 		// Test with different IP range that matches
426 | 		const resWithIP2 = await expoClient.signIn.email({
427 | 			email: testUser.email,
428 | 			password: testUser.password,
429 | 			callbackURL: "exp://192.168.1.100:8081/--/",
430 | 		});
431 | 		expect(resWithIP2.data?.user).toBeDefined();
432 | 
433 | 		// Test with different IP range that matches
434 | 		const resWithIP3 = await expoClient.signIn.email({
435 | 			email: testUser.email,
436 | 			password: testUser.password,
437 | 			callbackURL: "exp://172.16.0.1:8081/--/",
438 | 		});
439 | 		expect(resWithIP3.data?.user).toBeDefined();
440 | 
441 | 		// Test with IP that doesn't match any pattern - should fail
442 | 		const resWithUnmatchedIP = await expoClient.signIn.email({
443 | 			email: testUser.email,
444 | 			password: testUser.password,
445 | 			callbackURL: "exp://203.0.113.0:8081/--/",
446 | 		});
447 | 		expect(resWithUnmatchedIP.error?.status).toBe(403);
448 | 	});
449 | });
450 | 
451 | describe("origin check middleware", async (it) => {
452 | 	it("should return invalid origin", async () => {
453 | 		const { client } = await getTestInstance({
454 | 			trustedOrigins: ["https://trusted-site.com"],
455 | 			plugins: [
456 | 				{
457 | 					id: "test",
458 | 					endpoints: {
459 | 						test: createAuthEndpoint(
460 | 							"/test",
461 | 							{
462 | 								method: "GET",
463 | 								query: z.object({
464 | 									callbackURL: z.string(),
465 | 								}),
466 | 								use: [originCheck((c) => c.query.callbackURL)],
467 | 							},
468 | 							async (c) => {
469 | 								return c.query.callbackURL;
470 | 							},
471 | 						),
472 | 					},
473 | 				},
474 | 			],
475 | 		});
476 | 		const invalid = await client.$fetch(
477 | 			"/test?callbackURL=https://malicious-site.com",
478 | 		);
479 | 		expect(invalid.error?.status).toBe(403);
480 | 		const valid = await client.$fetch("/test?callbackURL=/dashboard");
481 | 		expect(valid.data).toBe("/dashboard");
482 | 		const validTrusted = await client.$fetch(
483 | 			"/test?callbackURL=https://trusted-site.com/path",
484 | 		);
485 | 		expect(validTrusted.data).toBe("https://trusted-site.com/path");
486 | 
487 | 		const sampleInternalEndpointInvalid = await client.$fetch(
488 | 			"/verify-email?callbackURL=https://malicious-site.com&token=xyz",
489 | 		);
490 | 		expect(sampleInternalEndpointInvalid.error?.status).toBe(403);
491 | 	});
492 | });
493 | 
494 | describe("is simple request", async (it) => {
495 | 	it("should return true for simple requests", async () => {
496 | 		const request = new Request("http://localhost:3000/test", {
497 | 			method: "GET",
498 | 		});
499 | 		const isSimple = isSimpleRequest(request.headers);
500 | 		expect(isSimple).toBe(true);
501 | 	});
502 | 
503 | 	it("should return false for non-simple requests", async () => {
504 | 		const request = new Request("http://localhost:3000/test", {
505 | 			method: "POST",
506 | 			headers: {
507 | 				"custom-header": "value",
508 | 			},
509 | 		});
510 | 		const isSimple = isSimpleRequest(request.headers);
511 | 		expect(isSimple).toBe(false);
512 | 	});
513 | 
514 | 	it("should return false for requests with a content type that is not simple", async () => {
515 | 		const request = new Request("http://localhost:3000/test", {
516 | 			method: "POST",
517 | 			headers: {
518 | 				"content-type": "application/json",
519 | 			},
520 | 		});
521 | 		const isSimple = isSimpleRequest(request.headers);
522 | 		expect(isSimple).toBe(false);
523 | 	});
524 | 
525 | 	it;
526 | });
527 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/integrations/waku.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Waku Integration
  3 | description: Integrate Better Auth with Waku.
  4 | ---
  5 | 
  6 | Better Auth can be easily integrated with Waku. Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
  7 | 
  8 | ## Create auth instance
  9 | 
 10 | Create a file named `auth.ts` in your application. Import Better Auth and create your instance.
 11 | 
 12 | <Callout type="warn">
 13 | Make sure to export the auth instance with the variable name `auth` or as a `default` export.
 14 | </Callout>
 15 | 
 16 | ```ts title="src/auth.ts"
 17 | import { betterAuth } from "better-auth"
 18 | 
 19 | export const auth = betterAuth({
 20 |     database: {
 21 |         provider: "postgres", //change this to your database provider
 22 |         url: process.env.DATABASE_URL, // path to your database or connection string
 23 |     }
 24 | })
 25 | ```
 26 | 
 27 | ## Create API Route
 28 | 
 29 | We need to mount the handler to a API route. Create a directory for Waku's file system router at `src/pages/api/auth`. Create a catch-all route file `[...route].ts` inside the `src/pages/api/auth` directory. And add the following code:
 30 | 
 31 | ```ts title="src/pages/api/auth/[...route].ts"
 32 | import { auth } from "../../../auth" // Adjust the path as necessary
 33 | 
 34 | export const GET = async (request: Request): Promise<Response> => {
 35 |   return auth.handler(request)
 36 | }
 37 | 
 38 | export const POST = async (request: Request): Promise<Response> => {
 39 |   return auth.handler(request)
 40 | }
 41 | ```
 42 | 
 43 | <Callout type="info">
 44 |  You can change the path on your better-auth configuration but it's recommended to keep it as `src/pages/api/auth/[...route].ts`
 45 | </Callout>
 46 | 
 47 | ## Create a client
 48 | 
 49 | Create a client instance. Here we are creating `auth-client.ts` file inside the `lib/` directory.
 50 | 
 51 | ```ts title="src/lib/auth-client.ts"
 52 | import { createAuthClient } from "better-auth/react" // make sure to import from better-auth/react
 53 | 
 54 | export const authClient = createAuthClient({
 55 |     //you can pass client configuration here
 56 | })
 57 | 
 58 | export type Session = typeof authClient.$Infer.Session // you can infer typescript types from the authClient
 59 | ```
 60 | 
 61 | Once you have created the client, you can use it to sign up, sign in, and perform other actions.
 62 | Some of the actions are reactive. The client uses [nano-store](https://github.com/nanostores/nanostores) to store the state and re-render the components when the state changes.
 63 | 
 64 | The client also uses [better-fetch](https://github.com/bekacru/better-fetch) to make the requests. You can pass the fetch configuration to the client.
 65 | 
 66 | ## RSC and Server actions
 67 | 
 68 | The `api` object exported from the auth instance contains all the actions that you can perform on the server. Every endpoint made inside Better Auth is a invocable as a function. Including plugins endpoints.
 69 | 
 70 | **Example: Getting Session on a server action**
 71 | 
 72 | ```tsx title="server.ts"
 73 | "use server" // Waku currently only supports file-level "use server"
 74 | 
 75 | import { auth } from "./auth"
 76 | import { getContext } from "waku/middleware/context"
 77 | 
 78 | export const someAuthenticatedAction = async () => {
 79 |   "use server"
 80 |   const session = await auth.api.getSession({
 81 |     headers: new Headers(getContext().req.headers),
 82 |   })
 83 | };
 84 | ```
 85 | 
 86 | **Example: Getting Session on a RSC**
 87 | 
 88 | 
 89 | ```tsx
 90 | import { auth } from "../auth"
 91 | import { getContext } from "waku/middleware/context"
 92 | 
 93 | export async function ServerComponent() {
 94 |     const session = await auth.api.getSession({
 95 |         headers: new Headers(getContext().req.headers),
 96 |     })
 97 |     if(!session) {
 98 |         return <div>Not authenticated</div>
 99 |     }
100 |     return (
101 |         <div>
102 |             <h1>Welcome {session.user.name}</h1>
103 |         </div>
104 |     )
105 | }
106 | ```
107 | 
108 | <Callout type="warn">RSCs that run after the response has started streaming cannot set cookies. The [cookie cache](/docs/concepts/session-management#cookie-cache) will not be refreshed until the server is interacted with from the client via Server Actions or Route Handlers.</Callout>
109 | 
110 | ### Server Action Cookies
111 | 
112 | When you call a function that needs to set cookies, like `signInEmail` or `signUpEmail` in a server action, cookies won’t be set.
113 | 
114 | We can create a plugin that works together with our middleware to set cookies.
115 | 
116 | ```ts title="auth.ts"
117 | import { betterAuth } from "better-auth";
118 | import { wakuCookies } from "better-auth/waku";
119 | import { getContextData } from "waku/middleware/context";
120 | 
121 | export const auth = betterAuth({
122 |     //...your config
123 |     plugins: [wakuCookies()] // make sure this is the last plugin in the array // [!code highlight]
124 | })
125 | 
126 | function wakuCookies() {
127 |   return {
128 |     id: "waku-cookies",
129 |     hooks: {
130 |       after: [
131 |         {
132 |           matcher(ctx) {
133 |             return true;
134 |           },
135 |           handler: createAuthMiddleware(async (ctx) => {
136 |             const returned = ctx.context.responseHeaders;
137 |             if ("_flag" in ctx && ctx._flag === "router") {
138 |               return;
139 |             }
140 |             if (returned instanceof Headers) {
141 |               const setCookieHeader = returned?.get("set-cookie");
142 |               if (!setCookieHeader) return;
143 |               const contextData = getContextData();
144 |               contextData.betterAuthSetCookie = setCookieHeader;
145 |             }
146 |           }),
147 |         },
148 |       ],
149 |     },
150 |   } satisfies BetterAuthPlugin;
151 | }
152 | ```
153 | 
154 | See below for the middleware to create to add the `contextData.betterAuthSetCookie` cookies to the response.
155 | Now, when you call functions that set cookies, they will be automatically set.
156 | 
157 | ```ts
158 | "use server";
159 | import { auth } from "../auth"
160 | 
161 | const signIn = async () => {
162 |     await auth.api.signInEmail({
163 |         body: {
164 |             email: "[email protected]",
165 |             password: "password",
166 |         }
167 |     })
168 | }
169 | ```
170 | 
171 | ### Middleware
172 | 
173 | In Waku middleware, it's recommended to only check for the existence of a session cookie to handle redirection. This avoids blocking requests by making API or database calls.
174 | 
175 | You can use the `getSessionCookie` helper from Better Auth for this purpose:
176 | 
177 | <Callout type="warn">
178 | The <code>getSessionCookie()</code> function does not automatically reference the auth config specified in <code>auth.ts</code>. Therefore, if you customized the cookie name or prefix, you need to ensure that the configuration in <code>getSessionCookie()</code> matches the config defined in your <code>auth.ts</code>.
179 | </Callout>
180 | 
181 | ```ts title="src/middleware/auth.ts"
182 | import type { Middleware } from "waku/config"
183 | import { getSession } from "../auth"
184 | import { getSessionCookie } from "better-auth/cookies"
185 | 
186 | const authMiddleware: Middleware = () => {
187 |     return async (ctx, next) => {
188 |         const sessionCookie = getSessionCookie(
189 |             new Request(ctx.req.url, {
190 |                 body: ctx.req.body,
191 |                 headers: ctx.req.headers,
192 |                 method: ctx.req.method,
193 |             })
194 |         )
195 |         // THIS IS NOT SECURE!
196 |         // This is the recommended approach to optimistically redirect users
197 |         // We recommend handling auth checks in each page/route
198 |         if (!sessionCookie && ctx.req.url.pathname !== "/") {
199 |             if (!ctx.req.url.pathname.endsWith(".txt")) {
200 |                 // Currently RSC requests end in .txt and don't handle redirect responses
201 |                 // The redirect needs to be encoded in the React flight stream somehow
202 |                 // There is some functionality in Waku to do this from a server component
203 |                 // but not from middleware.
204 |                 ctx.res.status = 302;
205 |                 ctx.res.headers = {
206 |                   Location: new URL("/", ctx.req.url).toString(),
207 |                 };
208 |             }
209 |         }
210 | 
211 |         // TODO possible to inspect ctx.req.url and not do this on every request
212 |         // Or skip starting the promise here and just invoke from server components and functions
213 |         getSession()
214 |         await next()
215 |         if (ctx.data.betterAuthSetCookie) {
216 |             ctx.res.headers ||= {}
217 |             let origSetCookie = ctx.res.headers["set-cookie"] || ([] as string[])
218 |             if (typeof origSetCookie === "string") {
219 |                 origSetCookie = [origSetCookie]
220 |             }
221 |             ctx.res.headers["set-cookie"] = [
222 |                 ...origSetCookie,
223 |                 ctx.data.betterAuthSetCookie as string,
224 |             ]
225 |         }
226 |     }
227 | };
228 | 
229 | export default authMiddleware;
230 | ```
231 | 
232 | <Callout type="warn">
233 | 	**Security Warning:** The `getSessionCookie` function only checks for the
234 | 	existence of a session cookie; it does **not** validate it. Relying solely
235 | 	on this check for security is dangerous, as anyone can manually create a
236 | 	cookie to bypass it. You must always validate the session on your server for
237 | 	any protected actions or pages.
238 | </Callout>
239 | 
240 | <Callout type="info">
241 | If you have a custom cookie name or prefix, you can pass it to the `getSessionCookie` function.
242 | ```ts
243 | const sessionCookie = getSessionCookie(request, {
244 |     cookieName: "my_session_cookie",
245 |     cookiePrefix: "my_prefix"
246 | })
247 | ```
248 | </Callout>
249 | 
250 | Alternatively, you can use the `getCookieCache` helper to get the session object from the cookie cache.
251 | 
252 | ```ts
253 | import { getCookieCache } from "better-auth/cookies"
254 | 
255 | const authMiddleware: Middleware = () => {
256 |     return async (ctx, next) => {
257 |         const session = await getCookieCache(ctx.req)
258 |         if (!session && ctx.req.url.pathname !== "/") {
259 |             if (!ctx.req.url.pathname.endsWith(".txt")) {
260 |                 ctx.res.status = 302
261 |                 ctx.res.headers = {
262 |                     Location: new URL("/", ctx.req.url).toString(),
263 |                 }
264 |             }
265 |         }
266 |     }
267 |     await next();
268 |   }
269 | }
270 | 
271 | export default authMiddleware;
272 | ```
273 | 
274 | Note that your middleware will need to be added to a waku.config.ts file (create this file if it doesn't already exist in your project):
275 | 
276 | ```ts title="waku.config.ts"
277 | import { defineConfig } from "waku/config";
278 | 
279 | export default defineConfig({
280 |   middleware: [
281 |     "waku/middleware/context",
282 |     "waku/middleware/dev-server",
283 |     "./src/middleware/auth.ts",
284 |     "waku/middleware/handler",
285 |   ],
286 | });
287 | ```
288 | 
289 | ### How to handle auth checks in each page/route
290 | 
291 | In this example, we are using the `auth.api.getSession` function within a server component to get the session object,
292 | then we are checking if the session is valid. If it's not, we are redirecting the user to the sign-in page.
293 | Waku has `getContext` to get the request headers and `getContextData()` to store data per request. We can use this
294 | to avoid fetching the session more than once per request.
295 | 
296 | ```ts title="auth.ts"
297 | import { getContext, getContextData } from "waku/middleware/context";
298 | 
299 | // Code from above to create the server auth config
300 | // export const auth = ...
301 | 
302 | export function getSession(): Promise<Session | null> {
303 |   const contextData = getContextData();
304 |   const ctx = getContext();
305 |   const existingSessionPromise = contextData.sessionPromise as
306 |     | Promise<Session | null>
307 |     | undefined;
308 |   if (existingSessionPromise) {
309 |     return existingSessionPromise;
310 |   }
311 |   const sessionPromise = auth.api.getSession({
312 |     headers: new Headers(ctx.req.headers),
313 |   });
314 |   contextData.sessionPromise = sessionPromise;
315 |   return sessionPromise;
316 | }
317 | ```
318 | 
319 | 
320 | ```tsx title="src/pages/dashboard.tsx"
321 | import { getSession } from "../auth";
322 | import { unstable_redirect as redirect } from 'waku/router/server';
323 | 
324 | export default async function DashboardPage() {
325 |     const session = await getSession()
326 | 
327 |     if (!session) {
328 |         redirect("/sign-in")
329 |     }
330 | 
331 |     return (
332 |         <div>
333 |             <h1>Welcome {session.user.name}</h1>
334 |         </div>
335 |     )
336 | }
337 | ```
338 | 
339 | ### Example usage
340 | 
341 | #### Sign Up
342 | 
343 | ```ts title="src/components/signup.tsx"
344 | "use client"
345 | 
346 | import { useState } from "react"
347 | import { authClient } from "../lib/auth-client"
348 | 
349 | export default function SignUp() {
350 |   const [email, setEmail] = useState("")
351 |   const [name, setName] = useState("")
352 |   const [password, setPassword] = useState("")
353 | 
354 |   const signUp = async () => {
355 |     await authClient.signUp.email(
356 |       {
357 |         email,
358 |         password,
359 |         name,
360 |       },
361 |       {
362 |         onRequest: (ctx) => {
363 |           // show loading state
364 |         },
365 |         onSuccess: (ctx) => {
366 |           // redirect to home
367 |         },
368 |         onError: (ctx) => {
369 |           alert(ctx.error)
370 |         },
371 |       },
372 |     )
373 |   }
374 | 
375 |   return (
376 |     <div>
377 |       <h2>
378 |         Sign Up
379 |       </h2>
380 |       <form
381 |         onSubmit={signUp}
382 |       >
383 |         <input
384 |           type="text"
385 |           value={name}
386 |           onChange={(e) => setName(e.target.value)}
387 |           placeholder="Name"
388 |         />
389 |         <input
390 |           type="email"
391 |           value={email}
392 |           onChange={(e) => setEmail(e.target.value)}
393 |           placeholder="Email"
394 |         />
395 |         <input
396 |           type="password"
397 |           value={password}
398 |           onChange={(e) => setPassword(e.target.value)}
399 |           placeholder="Password"
400 |         />
401 |         <button
402 |           type="submit"
403 |         >
404 |           Sign Up
405 |         </button>
406 |       </form>
407 |     </div>
408 |   )
409 | }
410 | 
411 | ```
412 | 
413 | #### Sign In
414 | 
415 | ```ts title="src/components/signin.tsx"
416 | "use client"
417 | 
418 | import { useState } from "react"
419 | import { authClient } from "../lib/auth-client"
420 | 
421 | export default function SignIn() {
422 |   const [email, setEmail] = useState("")
423 |   const [password, setPassword] = useState("")
424 | 
425 |   const signIn = async () => {
426 |     await authClient.signIn.email(
427 |       {
428 |         email,
429 |         password,
430 |       },
431 |       {
432 |         onRequest: (ctx) => {
433 |           // show loading state
434 |         },
435 |         onSuccess: (ctx) => {
436 |           // redirect to home
437 |         },
438 |         onError: (ctx) => {
439 |           alert(ctx.error)
440 |         },
441 |       },
442 |     )
443 |   }
444 | 
445 |   return (
446 |     <div>
447 |       <h2>
448 |         Sign In
449 |       </h2>
450 |       <form onSubmit={signIn}>
451 |         <input
452 |           type="email"
453 |           value={email}
454 |           onChange={(e) => setEmail(e.target.value)}
455 |         />
456 |         <input
457 |           type="password"
458 |           value={password}
459 |           onChange={(e) => setPassword(e.target.value)}
460 |         />
461 |         <button
462 |           type="submit"
463 |         >
464 |           Sign In
465 |         </button>
466 |       </form>
467 |     </div>
468 |   )
469 | }
470 | ```
471 | 
```

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

```typescript
  1 | import { describe, expect } from "vitest";
  2 | import { getTestInstance } from "../../test-utils/test-instance";
  3 | import { username } from ".";
  4 | import { usernameClient } from "./client";
  5 | 
  6 | describe("username", async (it) => {
  7 | 	const { client, sessionSetter, signInWithTestUser } = await getTestInstance(
  8 | 		{
  9 | 			plugins: [
 10 | 				username({
 11 | 					minUsernameLength: 4,
 12 | 				}),
 13 | 			],
 14 | 		},
 15 | 		{
 16 | 			clientOptions: {
 17 | 				plugins: [usernameClient()],
 18 | 			},
 19 | 		},
 20 | 	);
 21 | 
 22 | 	it("should sign up with username", async () => {
 23 | 		const headers = new Headers();
 24 | 		await client.signUp.email(
 25 | 			{
 26 | 				email: "[email protected]",
 27 | 				username: "new_username",
 28 | 				password: "new-password",
 29 | 				name: "new-name",
 30 | 			},
 31 | 			{
 32 | 				onSuccess: sessionSetter(headers),
 33 | 			},
 34 | 		);
 35 | 		const session = await client.getSession({
 36 | 			fetchOptions: {
 37 | 				headers,
 38 | 				throw: true,
 39 | 			},
 40 | 		});
 41 | 		expect(session?.user.username).toBe("new_username");
 42 | 	});
 43 | 	const headers = new Headers();
 44 | 	it("should sign-in with username", async () => {
 45 | 		const res = await client.signIn.username(
 46 | 			{
 47 | 				username: "new_username",
 48 | 				password: "new-password",
 49 | 			},
 50 | 			{
 51 | 				onSuccess: sessionSetter(headers),
 52 | 			},
 53 | 		);
 54 | 		expect(res.data?.token).toBeDefined();
 55 | 	});
 56 | 	it("should update username", async () => {
 57 | 		const res = await client.updateUser({
 58 | 			username: "new_username_2.1",
 59 | 			fetchOptions: {
 60 | 				headers,
 61 | 			},
 62 | 		});
 63 | 
 64 | 		const session = await client.getSession({
 65 | 			fetchOptions: {
 66 | 				headers,
 67 | 				throw: true,
 68 | 			},
 69 | 		});
 70 | 		expect(session?.user.username).toBe("new_username_2.1");
 71 | 	});
 72 | 
 73 | 	it("should fail on duplicate username in sign-up", async () => {
 74 | 		const res = await client.signUp.email({
 75 | 			email: "[email protected]",
 76 | 			username: "New_username_2.1",
 77 | 			password: "new_password",
 78 | 			name: "new-name",
 79 | 		});
 80 | 		expect(res.error?.status).toBe(422);
 81 | 	});
 82 | 
 83 | 	it("should fail on duplicate username in update-user if user is different", async () => {
 84 | 		const newHeaders = new Headers();
 85 | 		await client.signUp.email({
 86 | 			email: "[email protected]",
 87 | 			username: "duplicate-username",
 88 | 			password: "new_password",
 89 | 			name: "new-name",
 90 | 			fetchOptions: {
 91 | 				headers: newHeaders,
 92 | 			},
 93 | 		});
 94 | 
 95 | 		const { headers: testUserHeaders } = await signInWithTestUser();
 96 | 		const res = await client.updateUser({
 97 | 			username: "duplicate-username",
 98 | 			fetchOptions: {
 99 | 				headers: testUserHeaders,
100 | 			},
101 | 		});
102 | 		expect(res.error?.status).toBe(400);
103 | 	});
104 | 
105 | 	it("should succeed on duplicate username in update-user if user is the same", async () => {
106 | 		await client.updateUser({
107 | 			username: "New_username_2.1",
108 | 			fetchOptions: {
109 | 				headers,
110 | 			},
111 | 		});
112 | 
113 | 		const session = await client.getSession({
114 | 			fetchOptions: {
115 | 				headers,
116 | 				throw: true,
117 | 			},
118 | 		});
119 | 		expect(session?.user.username).toBe("new_username_2.1");
120 | 	});
121 | 
122 | 	it("should preserve both username and displayUsername when updating both", async () => {
123 | 		const updateRes = await client.updateUser({
124 | 			username: "priority_user",
125 | 			displayUsername: "Priority Display Name",
126 | 			fetchOptions: {
127 | 				headers,
128 | 			},
129 | 		});
130 | 
131 | 		expect(updateRes.error).toBeNull();
132 | 
133 | 		const session = await client.getSession({
134 | 			fetchOptions: {
135 | 				headers,
136 | 				throw: true,
137 | 			},
138 | 		});
139 | 
140 | 		expect(session?.user.username).toBe("priority_user");
141 | 		expect(session?.user.displayUsername).toBe("Priority Display Name");
142 | 	});
143 | 
144 | 	it("should fail on invalid username", async () => {
145 | 		const res = await client.signUp.email({
146 | 			email: "[email protected]",
147 | 			username: "new username",
148 | 			password: "new_password",
149 | 			name: "new-name",
150 | 		});
151 | 		expect(res.error?.status).toBe(400);
152 | 		expect(res.error?.code).toBe("USERNAME_IS_INVALID");
153 | 	});
154 | 
155 | 	it("should fail on too short username", async () => {
156 | 		const res = await client.signUp.email({
157 | 			email: "[email protected]",
158 | 			username: "new",
159 | 			password: "new_password",
160 | 			name: "new-name",
161 | 		});
162 | 		expect(res.error?.status).toBe(400);
163 | 		expect(res.error?.code).toBe("USERNAME_IS_TOO_SHORT");
164 | 	});
165 | 
166 | 	it("should fail on empty username", async () => {
167 | 		const res = await client.signUp.email({
168 | 			email: "[email protected]",
169 | 			username: "",
170 | 			password: "new_password",
171 | 			name: "new-name",
172 | 		});
173 | 		expect(res.error?.status).toBe(400);
174 | 	});
175 | 
176 | 	it("should check if username is unavailable", async () => {
177 | 		const res = await client.isUsernameAvailable({
178 | 			username: "priority_user",
179 | 		});
180 | 		expect(res.data?.available).toEqual(false);
181 | 	});
182 | 
183 | 	it("should check if username is unavailable with different case (normalization)", async () => {
184 | 		const res = await client.isUsernameAvailable({
185 | 			username: "PRIORITY_USER",
186 | 		});
187 | 		expect(res.data?.available).toEqual(false);
188 | 	});
189 | 
190 | 	it("should check if username is available", async () => {
191 | 		const res = await client.isUsernameAvailable({
192 | 			username: "new_username_2.2",
193 | 		});
194 | 		expect(res.data?.available).toEqual(true);
195 | 	});
196 | 
197 | 	it("should reject invalid username format in isUsernameAvailable", async () => {
198 | 		const res = await client.isUsernameAvailable({
199 | 			username: "invalid username!",
200 | 		});
201 | 		expect(res.error?.status).toBe(422);
202 | 		expect(res.error?.code).toBe("USERNAME_IS_INVALID");
203 | 	});
204 | 
205 | 	it("should reject too short username in isUsernameAvailable", async () => {
206 | 		const res = await client.isUsernameAvailable({
207 | 			username: "abc",
208 | 		});
209 | 		expect(res.error?.status).toBe(422);
210 | 		expect(res.error?.code).toBe("USERNAME_IS_TOO_SHORT");
211 | 	});
212 | 
213 | 	it("should reject too long username in isUsernameAvailable", async () => {
214 | 		const longUsername = "a".repeat(31);
215 | 		const res = await client.isUsernameAvailable({
216 | 			username: longUsername,
217 | 		});
218 | 		expect(res.error?.status).toBe(422);
219 | 		expect(res.error?.code).toBe("USERNAME_IS_TOO_LONG");
220 | 	});
221 | 
222 | 	it("should not normalize displayUsername", async () => {
223 | 		const headers = new Headers();
224 | 		await client.signUp.email(
225 | 			{
226 | 				email: "[email protected]",
227 | 				displayUsername: "Test Username",
228 | 				password: "test-password",
229 | 				name: "test-name",
230 | 			},
231 | 			{
232 | 				onSuccess: sessionSetter(headers),
233 | 			},
234 | 		);
235 | 
236 | 		const session = await client.getSession({
237 | 			fetchOptions: {
238 | 				headers,
239 | 				throw: true,
240 | 			},
241 | 		});
242 | 
243 | 		expect(session?.user.username).toBe("test username");
244 | 		expect(session?.user.displayUsername).toBe("Test Username");
245 | 	});
246 | 
247 | 	it("should preserve both username and displayUsername when both are provided", async () => {
248 | 		const headers = new Headers();
249 | 		await client.signUp.email(
250 | 			{
251 | 				email: "[email protected]",
252 | 				username: "custom_user",
253 | 				displayUsername: "Fancy Display Name",
254 | 				password: "test-password",
255 | 				name: "test-name",
256 | 			},
257 | 			{
258 | 				onSuccess: sessionSetter(headers),
259 | 			},
260 | 		);
261 | 
262 | 		const session = await client.getSession({
263 | 			fetchOptions: {
264 | 				headers,
265 | 				throw: true,
266 | 			},
267 | 		});
268 | 
269 | 		expect(session?.user.username).toBe("custom_user");
270 | 		expect(session?.user.displayUsername).toBe("Fancy Display Name");
271 | 	});
272 | 
273 | 	it("should sign in with normalized username", async () => {
274 | 		const { client } = await getTestInstance(
275 | 			{
276 | 				plugins: [username()],
277 | 			},
278 | 			{
279 | 				clientOptions: {
280 | 					plugins: [usernameClient()],
281 | 				},
282 | 			},
283 | 		);
284 | 		await client.signUp.email({
285 | 			email: "[email protected]",
286 | 			username: "Custom_User",
287 | 			password: "test-password",
288 | 			name: "test-name",
289 | 		});
290 | 		const res2 = await client.signIn.username({
291 | 			username: "Custom_User",
292 | 			password: "test-password",
293 | 		});
294 | 		expect(res2.data?.user.username).toBe("custom_user");
295 | 		expect(res2.data?.user.displayUsername).toBe("Custom_User");
296 | 	});
297 | });
298 | 
299 | describe("username custom normalization", async (it) => {
300 | 	const { client } = await getTestInstance(
301 | 		{
302 | 			plugins: [
303 | 				username({
304 | 					minUsernameLength: 4,
305 | 					usernameNormalization: (username) =>
306 | 						username.replaceAll("0", "o").replaceAll("4", "a").toLowerCase(),
307 | 				}),
308 | 			],
309 | 		},
310 | 		{
311 | 			clientOptions: {
312 | 				plugins: [usernameClient()],
313 | 			},
314 | 		},
315 | 	);
316 | 
317 | 	it("should sign up with username", async () => {
318 | 		const res = await client.signUp.email({
319 | 			email: "[email protected]",
320 | 			username: "H4XX0R",
321 | 			password: "new-password",
322 | 			name: "new-name",
323 | 		});
324 | 		expect(res.error).toBeNull();
325 | 	});
326 | 
327 | 	it("should fail on duplicate username", async () => {
328 | 		const res = await client.signUp.email({
329 | 			email: "[email protected]",
330 | 			username: "haxxor",
331 | 			password: "new-password",
332 | 			name: "new-name",
333 | 		});
334 | 		expect(res.error?.status).toBe(400);
335 | 	});
336 | 
337 | 	it("should normalize displayUsername", async () => {
338 | 		const { auth } = await getTestInstance({
339 | 			plugins: [
340 | 				username({
341 | 					displayUsernameNormalization: (displayUsername) =>
342 | 						displayUsername.toLowerCase(),
343 | 				}),
344 | 			],
345 | 		});
346 | 		const res = await auth.api.signUpEmail({
347 | 			body: {
348 | 				email: "[email protected]",
349 | 				password: "new-password",
350 | 				name: "new-name",
351 | 				username: "test_username",
352 | 				displayUsername: "Test Username",
353 | 			},
354 | 		});
355 | 		const session = await auth.api.getSession({
356 | 			headers: new Headers({
357 | 				authorization: `Bearer ${res.token}`,
358 | 			}),
359 | 		});
360 | 		expect(session?.user.username).toBe("test_username");
361 | 		expect(session?.user.displayUsername).toBe("test username");
362 | 	});
363 | });
364 | 
365 | describe("username with displayUsername validation", async (it) => {
366 | 	const { client, sessionSetter } = await getTestInstance(
367 | 		{
368 | 			plugins: [
369 | 				username({
370 | 					displayUsernameValidator: (displayUsername) =>
371 | 						/^[a-zA-Z0-9_-]+$/.test(displayUsername),
372 | 				}),
373 | 			],
374 | 		},
375 | 		{
376 | 			clientOptions: {
377 | 				plugins: [usernameClient()],
378 | 			},
379 | 		},
380 | 	);
381 | 
382 | 	it("should accept valid displayUsername", async () => {
383 | 		const res = await client.signUp.email({
384 | 			email: "[email protected]",
385 | 			displayUsername: "Valid_Display-123",
386 | 			password: "test-password",
387 | 			name: "test-name",
388 | 		});
389 | 		expect(res.error).toBeNull();
390 | 	});
391 | 
392 | 	it("should reject invalid displayUsername", async () => {
393 | 		const res = await client.signUp.email({
394 | 			email: "[email protected]",
395 | 			displayUsername: "Invalid Display!",
396 | 			password: "test-password",
397 | 			name: "test-name",
398 | 		});
399 | 		expect(res.error?.status).toBe(400);
400 | 		expect(res.error?.code).toBe("DISPLAY_USERNAME_IS_INVALID");
401 | 	});
402 | 
403 | 	it("should update displayUsername with valid value", async () => {
404 | 		const headers = new Headers();
405 | 		await client.signUp.email(
406 | 			{
407 | 				email: "[email protected]",
408 | 				displayUsername: "Initial_Name",
409 | 				password: "test-password",
410 | 				name: "test-name",
411 | 			},
412 | 			{
413 | 				onSuccess: sessionSetter(headers),
414 | 			},
415 | 		);
416 | 
417 | 		const sessionBefore = await client.getSession({
418 | 			fetchOptions: {
419 | 				headers,
420 | 				throw: true,
421 | 			},
422 | 		});
423 | 		expect(sessionBefore?.user.displayUsername).toBe("Initial_Name");
424 | 		expect(sessionBefore?.user.username).toBe("initial_name");
425 | 
426 | 		const res = await client.updateUser({
427 | 			displayUsername: "Updated_Name-123",
428 | 			fetchOptions: {
429 | 				headers,
430 | 			},
431 | 		});
432 | 
433 | 		expect(res.error).toBeNull();
434 | 		const sessionAfter = await client.getSession({
435 | 			fetchOptions: {
436 | 				headers,
437 | 				throw: true,
438 | 			},
439 | 		});
440 | 		expect(sessionAfter?.user.displayUsername).toBe("Updated_Name-123");
441 | 		expect(sessionAfter?.user.username).toBe("updated_name-123");
442 | 	});
443 | 
444 | 	it("should reject invalid displayUsername on update", async () => {
445 | 		const headers = new Headers();
446 | 		await client.signUp.email(
447 | 			{
448 | 				email: "[email protected]",
449 | 				displayUsername: "Valid_Name",
450 | 				password: "test-password",
451 | 				name: "test-name",
452 | 			},
453 | 			{
454 | 				onSuccess: sessionSetter(headers),
455 | 			},
456 | 		);
457 | 
458 | 		const res = await client.updateUser({
459 | 			displayUsername: "Invalid Display!",
460 | 			fetchOptions: {
461 | 				headers,
462 | 			},
463 | 		});
464 | 
465 | 		expect(res.error?.status).toBe(400);
466 | 		expect(res.error?.code).toBe("DISPLAY_USERNAME_IS_INVALID");
467 | 	});
468 | });
469 | 
470 | describe("isUsernameAvailable with custom validator", async (it) => {
471 | 	const { client } = await getTestInstance(
472 | 		{
473 | 			plugins: [
474 | 				username({
475 | 					usernameValidator: async (username) => {
476 | 						return username.startsWith("user_");
477 | 					},
478 | 				}),
479 | 			],
480 | 		},
481 | 		{
482 | 			clientOptions: {
483 | 				plugins: [usernameClient()],
484 | 			},
485 | 		},
486 | 	);
487 | 
488 | 	it("should accept username with custom validator", async () => {
489 | 		const res = await client.isUsernameAvailable({
490 | 			username: "user_valid123",
491 | 		});
492 | 		expect(res.data?.available).toEqual(true);
493 | 	});
494 | 
495 | 	it("should reject username that doesn't match custom validator", async () => {
496 | 		const res = await client.isUsernameAvailable({
497 | 			username: "invalid_user",
498 | 		});
499 | 		expect(res.error?.status).toBe(422);
500 | 		expect(res.error?.code).toBe("USERNAME_IS_INVALID");
501 | 	});
502 | });
503 | 
504 | describe("post normalization flow", async (it) => {
505 | 	it("should set displayUsername to username if only username is provided", async () => {
506 | 		const { auth } = await getTestInstance({
507 | 			plugins: [
508 | 				username({
509 | 					validationOrder: {
510 | 						username: "post-normalization",
511 | 						displayUsername: "post-normalization",
512 | 					},
513 | 					usernameNormalization: (username) => {
514 | 						return username.split(" ").join("_").toLowerCase();
515 | 					},
516 | 				}),
517 | 			],
518 | 		});
519 | 		const res = await auth.api.signUpEmail({
520 | 			body: {
521 | 				email: "[email protected]",
522 | 				username: "Test Username",
523 | 				password: "test-password",
524 | 				name: "test-name",
525 | 			},
526 | 		});
527 | 		const session = await auth.api.getSession({
528 | 			headers: new Headers({
529 | 				authorization: `Bearer ${res.token}`,
530 | 			}),
531 | 		});
532 | 		expect(session?.user.username).toBe("test_username");
533 | 		expect(session?.user.displayUsername).toBe("Test Username");
534 | 	});
535 | });
536 | 
537 | describe("username email verification flow (no info leak)", async (it) => {
538 | 	const { client } = await getTestInstance(
539 | 		{
540 | 			emailAndPassword: { enabled: true, requireEmailVerification: true },
541 | 			plugins: [username()],
542 | 		},
543 | 		{
544 | 			clientOptions: {
545 | 				plugins: [usernameClient()],
546 | 			},
547 | 		},
548 | 	);
549 | 
550 | 	it("returns INVALID_USERNAME_OR_PASSWORD for wrong password even if email is unverified", async () => {
551 | 		await client.signUp.email({
552 | 			email: "[email protected]",
553 | 			username: "unverified_user",
554 | 			password: "correct-password",
555 | 			name: "Unverified User",
556 | 		});
557 | 
558 | 		const res = await client.signIn.username({
559 | 			username: "unverified_user",
560 | 			password: "wrong-password",
561 | 		});
562 | 
563 | 		expect(res.error?.status).toBe(401);
564 | 		expect(res.error?.code).toBe("INVALID_USERNAME_OR_PASSWORD");
565 | 	});
566 | 
567 | 	it("returns EMAIL_NOT_VERIFIED only after a correct password for an unverified user", async () => {
568 | 		const res = await client.signIn.username({
569 | 			username: "unverified_user",
570 | 			password: "correct-password",
571 | 		});
572 | 
573 | 		expect(res.error?.status).toBe(403);
574 | 		expect(res.error?.code).toBe("EMAIL_NOT_VERIFIED");
575 | 	});
576 | });
577 | 
```
Page 36/71FirstPrevNextLast