#
tokens: 47802/50000 9/1100 files (page 25/51)
lines: off (toggle) GitHub
raw markdown copy
This is page 25 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/docs/components/ai-chat-modal.tsx:
--------------------------------------------------------------------------------

```typescript
"use client";

import { useState, useRef, useEffect } from "react";
import {
	Dialog,
	DialogContent,
	DialogHeader,
	DialogTitle,
	DialogDescription,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { Send, Bot, User, AlertCircle } from "lucide-react";
import { MarkdownRenderer } from "./markdown-renderer";
import { betterFetch } from "@better-fetch/fetch";
import { atom } from "jotai";

interface Message {
	id: string;
	role: "user" | "assistant";
	content: string;
	timestamp: Date;
	isStreaming?: boolean;
}

export const aiChatModalAtom = atom(false);

interface AIChatModalProps {
	isOpen: boolean;
	onClose: () => void;
}

export function AIChatModal({ isOpen, onClose }: AIChatModalProps) {
	const [messages, setMessages] = useState<Message[]>([]);
	const [input, setInput] = useState("");
	const [isLoading, setIsLoading] = useState(false);
	const [apiError, setApiError] = useState<string | null>(null);
	const [sessionId, setSessionId] = useState<string | null>(null);
	const [externalUserId] = useState<string>(
		() =>
			`better-auth-user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
	);
	const messagesEndRef = useRef<HTMLDivElement>(null);
	const abortControllerRef = useRef<AbortController | null>(null);

	const scrollToBottom = () => {
		messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
	};

	useEffect(() => {
		scrollToBottom();
	}, [messages]);

	useEffect(() => {
		return () => {
			if (abortControllerRef.current) {
				abortControllerRef.current.abort();
			}
		};
	}, []);

	useEffect(() => {
		if (!isOpen) {
			setSessionId(null);
			setMessages([]);
			setInput("");
			setApiError(null);
		}
	}, [isOpen]);

	const handleSubmit = async (e: React.FormEvent) => {
		e.preventDefault();
		if (!input.trim() || isLoading) return;

		const userMessage: Message = {
			id: Date.now().toString(),
			role: "user",
			content: input.trim(),
			timestamp: new Date(),
		};

		setMessages((prev) => [...prev, userMessage]);
		setInput("");
		setIsLoading(true);
		setApiError(null);

		const thinkingMessage: Message = {
			id: `thinking-${Date.now()}`,
			role: "assistant",
			content: "",
			timestamp: new Date(),
			isStreaming: false,
		};

		setMessages((prev) => [...prev, thinkingMessage]);

		abortControllerRef.current = new AbortController();

		try {
			const payload = {
				question: userMessage.content,
				stream: false, // Use non-streaming to get session_id
				session_id: sessionId, // Use existing session_id if available
				external_user_id: externalUserId, // Use consistent external_user_id for consistency on getting the context right
				fetch_existing: false,
			};

			const { data, error } = await betterFetch<{
				content?: string;
				answer?: string;
				response?: string;
				session_id?: string;
			}>("/api/ai-chat", {
				method: "POST",
				headers: {
					"content-type": "application/json",
				},
				body: JSON.stringify(payload),
				signal: abortControllerRef.current.signal,
			});

			if (error) {
				console.error("API Error Response:", error);
				throw new Error(`HTTP ${error.status}: ${error.message}`);
			}

			if (data.session_id) {
				setSessionId(data.session_id);
			}

			let answer = "";
			if (data.content) {
				answer = data.content;
			} else if (data.answer) {
				answer = data.answer;
			} else if (data.response) {
				answer = data.response;
			} else if (typeof data === "string") {
				answer = data;
			} else {
				console.error("Unexpected response format:", data);
				throw new Error("Unexpected response format from API");
			}

			await simulateStreamingEffect(answer, thinkingMessage.id);
		} catch (error) {
			if (error instanceof Error && error.name === "AbortError") {
				console.log("Request was aborted");
				return;
			}

			console.error("Error calling AI API:", error);

			setMessages((prev) =>
				prev.map((msg) =>
					msg.id.startsWith("thinking-")
						? {
								id: (Date.now() + 1).toString(),
								role: "assistant" as const,
								content: `I encountered an error while processing your request. Please try again.`,
								timestamp: new Date(),
								isStreaming: false,
							}
						: msg,
				),
			);

			if (error instanceof Error) {
				setApiError(error.message);
			}
		} finally {
			setIsLoading(false);
			abortControllerRef.current = null;
		}
	};

	const simulateStreamingEffect = async (
		fullContent: string,
		thinkingMessageId: string,
	) => {
		const assistantMessageId = (Date.now() + 1).toString();
		let displayedContent = "";

		setMessages((prev) =>
			prev.map((msg) =>
				msg.id === thinkingMessageId
					? {
							id: assistantMessageId,
							role: "assistant" as const,
							content: "",
							timestamp: new Date(),
							isStreaming: true,
						}
					: msg,
			),
		);

		const words = fullContent.split(" ");
		for (let i = 0; i < words.length; i++) {
			displayedContent += (i > 0 ? " " : "") + words[i];

			setMessages((prev) =>
				prev.map((msg) =>
					msg.id === assistantMessageId
						? { ...msg, content: displayedContent }
						: msg,
				),
			);

			const delay = Math.random() * 50 + 20;
			await new Promise((resolve) => setTimeout(resolve, delay));
		}

		setMessages((prev) =>
			prev.map((msg) =>
				msg.id === assistantMessageId ? { ...msg, isStreaming: false } : msg,
			),
		);
	};

	return (
		<Dialog open={isOpen} onOpenChange={onClose}>
			<DialogContent className="max-w-4xl border-b h-[80vh] flex flex-col">
				<DialogHeader>
					<DialogTitle className="flex items-center gap-2">
						<Bot className="h-5 w-5 text-primary" />
						Ask AI About Better Auth
					</DialogTitle>
					<DialogDescription>
						Ask questions about Better-Auth and get AI-powered answers
						{apiError && (
							<div className="flex items-center gap-2 mt-2 text-amber-600 dark:text-amber-400">
								<AlertCircle className="h-4 w-4" />
								<span className="text-xs">
									API Error: Something went wrong. Please try again.
								</span>
							</div>
						)}
					</DialogDescription>
				</DialogHeader>

				<div className="flex-1 flex flex-col min-h-0">
					<div
						className={cn(
							"flex-1 overflow-y-auto space-y-4 p-6",
							messages.length === 0 ? "overflow-y-hidden" : "overflow-y-auto",
						)}
					>
						{messages.length === 0 ? (
							<div className="flex h-full flex-col items-center justify-center text-center">
								<div className="mb-6">
									<div className="w-16 h-16 mx-auto bg-transparent border border-input/70 border-dashed rounded-none flex items-center justify-center mb-4">
										<Bot className="h-8 w-8 text-primary" />
									</div>
								</div>

								<div className="mb-8 max-w-md">
									<h3 className="text-xl font-semibold text-foreground mb-2">
										Ask About Better Auth
									</h3>
									<p className="text-muted-foreground text-sm leading-relaxed">
										I'm here to help you with Better Auth questions, setup
										guides, and implementation tips. Ask me anything!
									</p>
								</div>

								<div className="w-full max-w-lg">
									<p className="text-sm font-medium text-foreground mb-4">
										Try asking:
									</p>
									<div className="space-y-3">
										{[
											"How do I set up SSO with Google?",
											"How to integrate Better Auth with NextJs?",
											"How to setup Two Factor Authentication?",
										].map((question, index) => (
											<button
												key={index}
												onClick={() => setInput(question)}
												className="w-full text-left p-3 rounded-none border border-border/50 hover:border-primary/50 hover:bg-primary/5 transition-all duration-200 group"
											>
												<div className="flex items-center gap-3">
													<div className="w-6 h-6 rounded-none bg-transparent border border-input/70 border-dashed flex items-center justify-center group-hover:bg-primary/20 transition-colors">
														<span className="text-xs text-primary font-medium">
															{index + 1}
														</span>
													</div>
													<span className="text-sm text-foreground group-hover:text-primary transition-colors">
														{question}
													</span>
												</div>
											</button>
										))}
									</div>
								</div>
							</div>
						) : (
							messages.map((message) => (
								<div
									key={message.id}
									className={cn(
										"flex gap-3",
										message.role === "user" ? "justify-end" : "justify-start",
									)}
								>
									{message.role === "assistant" && (
										<div className="flex-shrink-0">
											<div className="w-8 h-8 rounded-full bg-transparent border border-input/70 border-dashed flex items-center justify-center">
												<Bot className="h-4 w-4 text-primary" />
											</div>
										</div>
									)}
									<div
										className={cn(
											"max-w-[80%] rounded-xl px-4 py-3 shadow-sm",
											message.role === "user"
												? "bg-primary text-primary-foreground"
												: "bg-background border border-border/50",
										)}
									>
										{message.role === "assistant" ? (
											<div className="w-full">
												{message.id.startsWith("thinking-") ? (
													<div className="flex items-center gap-2 text-sm text-muted-foreground">
														<div className="flex space-x-1">
															<div className="w-1 h-1 bg-primary rounded-full animate-bounce [animation-delay:-0.3s]"></div>
															<div className="w-1 h-1 bg-primary rounded-full animate-bounce [animation-delay:-0.15s]"></div>
															<div className="w-1 h-1 bg-primary rounded-full animate-bounce"></div>
														</div>
														<span>Thinking...</span>
													</div>
												) : (
													<>
														<MarkdownRenderer content={message.content} />
														{message.isStreaming && (
															<div className="inline-block w-2 h-4 bg-primary streaming-cursor ml-1" />
														)}
													</>
												)}
											</div>
										) : (
											<p className="text-sm">{message.content}</p>
										)}
									</div>
									{message.role === "user" && (
										<div className="flex-shrink-0">
											<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
												<User className="h-4 w-4" />
											</div>
										</div>
									)}
								</div>
							))
						)}
						<div ref={messagesEndRef} />
					</div>

					<div className="border-t px-0 bg-background/50 backdrop-blur-sm p-4">
						<div className="relative max-w-4xl mx-auto">
							<div
								className={cn(
									"relative flex flex-col border-input rounded-lg transition-all duration-200 w-full text-left",
									"ring-1 ring-border/20 bg-muted/30 border-input border-1 backdrop-blur-sm",
									"focus-within:ring-primary/30 focus-within:bg-muted/[35%]",
								)}
							>
								<div className="overflow-y-auto max-h-[200px]">
									<Textarea
										value={input}
										onChange={(e) => setInput(e.target.value)}
										placeholder="Ask a question about Better-Auth..."
										className="w-full rounded-none rounded-b-none px-4 py-3 h-[70px] bg-transparent border-none text-foreground placeholder:text-muted-foreground resize-none focus-visible:ring-0 leading-[1.2] min-h-[52px] max-h-32"
										disabled={isLoading}
										onKeyDown={(e) => {
											if (e.key === "Enter" && !e.shiftKey) {
												e.preventDefault();
												void handleSubmit(e);
											}
										}}
									/>
								</div>

								<div className="h-12 bg-muted/20 rounded-b-xl flex items-center justify-end px-3">
									<button
										type="submit"
										onClick={(e) => {
											e.preventDefault();
											void handleSubmit(e);
										}}
										disabled={!input.trim() || isLoading}
										className={cn(
											"rounded-lg p-2 transition-all duration-200",
											input.trim() && !isLoading
												? "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md"
												: "bg-muted/50 text-muted-foreground cursor-not-allowed",
										)}
									>
										{isLoading ? (
											<div className="w-4 h-4 border-2 border-current/30 border-t-current rounded-full animate-spin" />
										) : (
											<Send className="h-4 w-4" />
										)}
									</button>
								</div>
							</div>
						</div>

						<div className="mt-3 text-center">
							<p className="text-xs text-muted-foreground">
								Press{" "}
								<kbd className="px-1.5 py-0.5 text-xs bg-muted rounded">
									Enter
								</kbd>{" "}
								to send,{" "}
								<kbd className="px-1.5 py-0.5 text-xs bg-muted rounded">
									Shift+Enter
								</kbd>{" "}
								for new line
							</p>
						</div>
					</div>
				</div>
			</DialogContent>
		</Dialog>
	);
}

```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from "zod";
import { APIError, getSessionFromCtx } from "../../../api";
import { createAuthEndpoint } from "@better-auth/core/api";
import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
import { getDate } from "../../../utils/date";
import { apiKeySchema } from "../schema";
import type { ApiKey } from "../types";
import type { PredefinedApiKeyOptions } from ".";
import { safeJSONParse } from "../../../utils/json";
import { defaultKeyHasher } from "../";
import type { AuthContext } from "@better-auth/core";

export function createApiKey({
	keyGenerator,
	opts,
	schema,
	deleteAllExpiredApiKeys,
}: {
	keyGenerator: (options: {
		length: number;
		prefix: string | undefined;
	}) => Promise<string> | string;
	opts: PredefinedApiKeyOptions;
	schema: ReturnType<typeof apiKeySchema>;
	deleteAllExpiredApiKeys(
		ctx: AuthContext,
		byPassLastCheckTime?: boolean,
	): void;
}) {
	return createAuthEndpoint(
		"/api-key/create",
		{
			method: "POST",
			body: z.object({
				name: z
					.string()
					.meta({ description: "Name of the Api Key" })
					.optional(),
				expiresIn: z
					.number()
					.meta({
						description: "Expiration time of the Api Key in seconds",
					})
					.min(1)
					.optional()
					.nullable()
					.default(null),

				userId: z.coerce
					.string()
					.meta({
						description:
							'User Id of the user that the Api Key belongs to. server-only. Eg: "user-id"',
					})
					.optional(),
				prefix: z
					.string()
					.meta({ description: "Prefix of the Api Key" })
					.regex(/^[a-zA-Z0-9_-]+$/, {
						message:
							"Invalid prefix format, must be alphanumeric and contain only underscores and hyphens.",
					})
					.optional(),
				remaining: z
					.number()
					.meta({
						description: "Remaining number of requests. Server side only",
					})
					.min(0)
					.optional()
					.nullable()
					.default(null),
				metadata: z.any().optional(),
				refillAmount: z
					.number()
					.meta({
						description:
							"Amount to refill the remaining count of the Api Key. server-only. Eg: 100",
					})
					.min(1)
					.optional(),
				refillInterval: z
					.number()
					.meta({
						description:
							"Interval to refill the Api Key in milliseconds. server-only. Eg: 1000",
					})
					.optional(),
				rateLimitTimeWindow: z
					.number()
					.meta({
						description:
							"The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 1000",
					})
					.optional(),
				rateLimitMax: z
					.number()
					.meta({
						description:
							"Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100",
					})
					.optional(),
				rateLimitEnabled: z
					.boolean()
					.meta({
						description:
							"Whether the key has rate limiting enabled. server-only. Eg: true",
					})
					.optional(),
				permissions: z
					.record(z.string(), z.array(z.string()))
					.meta({
						description: "Permissions of the Api Key.",
					})
					.optional(),
			}),
			metadata: {
				openapi: {
					description: "Create a new API key for a user",
					responses: {
						"200": {
							description: "API key created successfully",
							content: {
								"application/json": {
									schema: {
										type: "object",
										properties: {
											id: {
												type: "string",
												description: "Unique identifier of the API key",
											},
											createdAt: {
												type: "string",
												format: "date-time",
												description: "Creation timestamp",
											},
											updatedAt: {
												type: "string",
												format: "date-time",
												description: "Last update timestamp",
											},
											name: {
												type: "string",
												nullable: true,
												description: "Name of the API key",
											},
											prefix: {
												type: "string",
												nullable: true,
												description: "Prefix of the API key",
											},
											start: {
												type: "string",
												nullable: true,
												description:
													"Starting characters of the key (if configured)",
											},
											key: {
												type: "string",
												description:
													"The full API key (only returned on creation)",
											},
											enabled: {
												type: "boolean",
												description: "Whether the key is enabled",
											},
											expiresAt: {
												type: "string",
												format: "date-time",
												nullable: true,
												description: "Expiration timestamp",
											},
											userId: {
												type: "string",
												description: "ID of the user owning the key",
											},
											lastRefillAt: {
												type: "string",
												format: "date-time",
												nullable: true,
												description: "Last refill timestamp",
											},
											lastRequest: {
												type: "string",
												format: "date-time",
												nullable: true,
												description: "Last request timestamp",
											},
											metadata: {
												type: "object",
												nullable: true,
												additionalProperties: true,
												description: "Metadata associated with the key",
											},
											rateLimitMax: {
												type: "number",
												nullable: true,
												description: "Maximum requests in time window",
											},
											rateLimitTimeWindow: {
												type: "number",
												nullable: true,
												description: "Rate limit time window in milliseconds",
											},
											remaining: {
												type: "number",
												nullable: true,
												description: "Remaining requests",
											},
											refillAmount: {
												type: "number",
												nullable: true,
												description: "Amount to refill",
											},
											refillInterval: {
												type: "number",
												nullable: true,
												description: "Refill interval in milliseconds",
											},
											rateLimitEnabled: {
												type: "boolean",
												description: "Whether rate limiting is enabled",
											},
											requestCount: {
												type: "number",
												description: "Current request count in window",
											},
											permissions: {
												type: "object",
												nullable: true,
												additionalProperties: {
													type: "array",
													items: { type: "string" },
												},
												description: "Permissions associated with the key",
											},
										},
										required: [
											"id",
											"createdAt",
											"updatedAt",
											"key",
											"enabled",
											"userId",
											"rateLimitEnabled",
											"requestCount",
										],
									},
								},
							},
						},
					},
				},
			},
		},
		async (ctx) => {
			const {
				name,
				expiresIn,
				prefix,
				remaining,
				metadata,
				refillAmount,
				refillInterval,
				permissions,
				rateLimitMax,
				rateLimitTimeWindow,
				rateLimitEnabled,
			} = ctx.body;

			const session = await getSessionFromCtx(ctx);
			const authRequired = ctx.request || ctx.headers;
			const user =
				authRequired && !session
					? null
					: session?.user || { id: ctx.body.userId };

			if (!user?.id) {
				throw new APIError("UNAUTHORIZED", {
					message: ERROR_CODES.UNAUTHORIZED_SESSION,
				});
			}

			if (session && ctx.body.userId && session?.user.id !== ctx.body.userId) {
				throw new APIError("UNAUTHORIZED", {
					message: ERROR_CODES.UNAUTHORIZED_SESSION,
				});
			}

			if (authRequired) {
				// if this endpoint was being called from the client,
				// we must make sure they can't use server-only properties.
				if (
					refillAmount !== undefined ||
					refillInterval !== undefined ||
					rateLimitMax !== undefined ||
					rateLimitTimeWindow !== undefined ||
					rateLimitEnabled !== undefined ||
					permissions !== undefined ||
					remaining !== null
				) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.SERVER_ONLY_PROPERTY,
					});
				}
			}

			// if metadata is defined, than check that it's an object.
			if (metadata) {
				if (opts.enableMetadata === false) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.METADATA_DISABLED,
					});
				}
				if (typeof metadata !== "object") {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.INVALID_METADATA_TYPE,
					});
				}
			}

			// make sure that if they pass a refill amount, they also pass a refill interval
			if (refillAmount && !refillInterval) {
				throw new APIError("BAD_REQUEST", {
					message: ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
				});
			}
			// make sure that if they pass a refill interval, they also pass a refill amount
			if (refillInterval && !refillAmount) {
				throw new APIError("BAD_REQUEST", {
					message: ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
				});
			}

			if (expiresIn) {
				if (opts.keyExpiration.disableCustomExpiresTime === true) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.KEY_DISABLED_EXPIRATION,
					});
				}

				const expiresIn_in_days = expiresIn / (60 * 60 * 24);

				if (opts.keyExpiration.minExpiresIn > expiresIn_in_days) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
					});
				} else if (opts.keyExpiration.maxExpiresIn < expiresIn_in_days) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
					});
				}
			}
			if (prefix) {
				if (prefix.length < opts.minimumPrefixLength) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.INVALID_PREFIX_LENGTH,
					});
				}
				if (prefix.length > opts.maximumPrefixLength) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.INVALID_PREFIX_LENGTH,
					});
				}
			}

			if (name) {
				if (name.length < opts.minimumNameLength) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.INVALID_NAME_LENGTH,
					});
				}
				if (name.length > opts.maximumNameLength) {
					throw new APIError("BAD_REQUEST", {
						message: ERROR_CODES.INVALID_NAME_LENGTH,
					});
				}
			} else if (opts.requireName) {
				throw new APIError("BAD_REQUEST", {
					message: ERROR_CODES.NAME_REQUIRED,
				});
			}

			deleteAllExpiredApiKeys(ctx.context);

			const key = await keyGenerator({
				length: opts.defaultKeyLength,
				prefix: prefix || opts.defaultPrefix,
			});

			const hashed = opts.disableKeyHashing ? key : await defaultKeyHasher(key);

			let start: string | null = null;

			if (opts.startingCharactersConfig.shouldStore) {
				start = key.substring(
					0,
					opts.startingCharactersConfig.charactersLength,
				);
			}

			const defaultPermissions = opts.permissions?.defaultPermissions
				? typeof opts.permissions.defaultPermissions === "function"
					? await opts.permissions.defaultPermissions(user.id, ctx)
					: opts.permissions.defaultPermissions
				: undefined;
			const permissionsToApply = permissions
				? JSON.stringify(permissions)
				: defaultPermissions
					? JSON.stringify(defaultPermissions)
					: undefined;

			let data: Omit<ApiKey, "id"> = {
				createdAt: new Date(),
				updatedAt: new Date(),
				name: name ?? null,
				prefix: prefix ?? opts.defaultPrefix ?? null,
				start: start,
				key: hashed,
				enabled: true,
				expiresAt: expiresIn
					? getDate(expiresIn, "sec")
					: opts.keyExpiration.defaultExpiresIn
						? getDate(opts.keyExpiration.defaultExpiresIn, "sec")
						: null,
				userId: user.id,
				lastRefillAt: null,
				lastRequest: null,
				metadata: null,
				rateLimitMax: rateLimitMax ?? opts.rateLimit.maxRequests ?? null,
				rateLimitTimeWindow:
					rateLimitTimeWindow ?? opts.rateLimit.timeWindow ?? null,
				remaining:
					remaining === null ? remaining : (remaining ?? refillAmount ?? null),
				refillAmount: refillAmount ?? null,
				refillInterval: refillInterval ?? null,
				rateLimitEnabled:
					rateLimitEnabled === undefined
						? (opts.rateLimit.enabled ?? true)
						: rateLimitEnabled,
				requestCount: 0,
				//@ts-expect-error - we intentionally save the permissions as string on DB.
				permissions: permissionsToApply,
			};

			if (metadata) {
				//@ts-expect-error - we intentionally save the metadata as string on DB.
				data.metadata = schema.apikey.fields.metadata.transform.input(metadata);
			}

			const apiKey = await ctx.context.adapter.create<
				Omit<ApiKey, "id">,
				ApiKey
			>({
				model: API_KEY_TABLE_NAME,
				data: data,
			});

			return ctx.json({
				...(apiKey as ApiKey),
				key: key,
				metadata: metadata ?? null,
				permissions: apiKey.permissions
					? safeJSONParse(apiKey.permissions)
					: null,
			});
		},
	);
}

```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/to-auth-endpoints.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, expect, it } from "vitest";
import {
	createAuthEndpoint,
	createAuthMiddleware,
} from "@better-auth/core/api";
import { toAuthEndpoints } from "./to-auth-endpoints";
import { init } from "../init";
import * as z from "zod";
import { APIError } from "better-call";
import { getTestInstance } from "../test-utils/test-instance";

describe("before hook", async () => {
	describe("context", async () => {
		const endpoints = {
			query: createAuthEndpoint(
				"/query",
				{
					method: "GET",
				},
				async (c) => {
					return c.query;
				},
			),
			body: createAuthEndpoint(
				"/body",
				{
					method: "POST",
				},
				async (c) => {
					return c.body;
				},
			),
			params: createAuthEndpoint(
				"/params",
				{
					method: "GET",
				},
				async (c) => {
					return c.params;
				},
			),
			headers: createAuthEndpoint(
				"/headers",
				{
					method: "GET",
					requireHeaders: true,
				},
				async (c) => {
					return Object.fromEntries(c.headers.entries());
				},
			),
		};

		const authContext = init({
			hooks: {
				before: createAuthMiddleware(async (c) => {
					switch (c.path) {
						case "/body":
							return {
								context: {
									body: {
										name: "body",
									},
								},
							};
						case "/params":
							return {
								context: {
									params: {
										name: "params",
									},
								},
							};
						case "/headers":
							return {
								context: {
									headers: new Headers({
										name: "headers",
									}),
								},
							};
					}
					return {
						context: {
							query: {
								name: "query",
							},
						},
					};
				}),
			},
		});
		const authEndpoints = toAuthEndpoints(endpoints, authContext);

		it("should return hook set query", async () => {
			const res = await authEndpoints.query();
			expect(res?.name).toBe("query");
			const res2 = await authEndpoints.query({
				query: {
					key: "value",
				},
			});
			expect(res2).toMatchObject({
				name: "query",
				key: "value",
			});
		});

		it("should return hook set body", async () => {
			const res = await authEndpoints.body();
			expect(res?.name).toBe("body");
			const res2 = await authEndpoints.body({
				//@ts-expect-error
				body: {
					key: "value",
				},
			});
			expect(res2).toMatchObject({
				name: "body",
				key: "value",
			});
		});

		it("should return hook set param", async () => {
			const res = await authEndpoints.params();
			expect(res?.name).toBe("params");
			const res2 = await authEndpoints.params({
				params: {
					key: "value",
				},
			});
			expect(res2).toMatchObject({
				name: "params",
				key: "value",
			});
		});

		it("should return hook set headers", async () => {
			const res = await authEndpoints.headers({
				headers: new Headers({
					key: "value",
				}),
			});
			expect(res).toMatchObject({ key: "value", name: "headers" });
		});

		it("should replace existing array when hook provides another array", async () => {
			const endpoint = {
				body: createAuthEndpoint(
					"/body-array-replace",
					{ method: "POST", body: z.object({ tags: z.array(z.string()) }) },
					async (c) => c.body,
				),
			};
			const authContext = init({
				hooks: {
					before: createAuthMiddleware(async (c) => {
						if (c.path === "/body-array-replace") {
							return {
								context: {
									body: {
										tags: ["a"],
									},
								},
							};
						}
					}),
				},
			});
			const api = toAuthEndpoints(endpoint, authContext);

			const res = await api.body({
				body: {
					tags: ["b", "c"],
				},
			});
			expect(res.tags).toEqual(["a"]);
		});
	});

	describe("response", async () => {
		const endpoints = {
			response: createAuthEndpoint(
				"/response",
				{
					method: "GET",
				},
				async (c) => {
					return { response: true };
				},
			),
			json: createAuthEndpoint(
				"/json",
				{
					method: "GET",
				},
				async (c) => {
					return { response: true };
				},
			),
		};

		const authContext = init({
			hooks: {
				before: createAuthMiddleware(async (c) => {
					if (c.path === "/json") {
						return { before: true };
					}
					return new Response(JSON.stringify({ before: true }));
				}),
			},
		});
		const authEndpoints = toAuthEndpoints(endpoints, authContext);

		it("should return Response object", async () => {
			const response = await authEndpoints.response();
			expect(response).toBeInstanceOf(Response);
		});

		it("should return the hook response", async () => {
			const response = await authEndpoints.json();
			expect(response).toMatchObject({ before: true });
		});
	});
});

describe("after hook", async () => {
	describe("response", async () => {
		const endpoints = {
			changeResponse: createAuthEndpoint(
				"/change-response",
				{
					method: "GET",
				},
				async (c) => {
					return {
						hello: "world",
					};
				},
			),
			throwError: createAuthEndpoint(
				"/throw-error",
				{
					method: "POST",
					query: z
						.object({
							throwHook: z.boolean(),
						})
						.optional(),
				},
				async (c) => {
					throw c.error("BAD_REQUEST");
				},
			),
			multipleHooks: createAuthEndpoint(
				"/multi-hooks",
				{
					method: "GET",
				},
				async (c) => {
					return {
						return: "1",
					};
				},
			),
		};

		const authContext = init({
			plugins: [
				{
					id: "test",
					hooks: {
						after: [
							{
								matcher() {
									return true;
								},
								handler: createAuthMiddleware(async (c) => {
									if (c.path === "/multi-hooks") {
										return {
											return: "3",
										};
									}
								}),
							},
						],
					},
				},
			],
			hooks: {
				after: createAuthMiddleware(async (c) => {
					if (c.path === "/change-response") {
						return {
							hello: "auth",
						};
					}
					if (c.path === "/multi-hooks") {
						return {
							return: "2",
						};
					}
					if (c.query?.throwHook) {
						throw c.error("BAD_REQUEST", {
							message: "from after hook",
						});
					}
				}),
			},
		});

		const api = toAuthEndpoints(endpoints, authContext);

		it("should change the response object from `hello:world` to `hello:auth`", async () => {
			const response = await api.changeResponse();
			expect(response).toMatchObject({ hello: "auth" });
		});

		it("should return the last hook returned response", async () => {
			const response = await api.multipleHooks();
			expect(response).toMatchObject({
				return: "3",
			});
		});

		it("should return error as response", async () => {
			const response = await api.throwError({
				asResponse: true,
			});
			expect(response.status).toBe(400);
		});

		it("should throw the last error", async () => {
			await api
				.throwError({
					query: {
						throwHook: true,
					},
				})
				.catch((e) => {
					expect(e).toBeInstanceOf(APIError);
					expect(e?.message).toBe("from after hook");
				});
		});
	});

	describe("cookies", async () => {
		const endpoints = {
			cookies: createAuthEndpoint(
				"/cookies",
				{
					method: "POST",
				},
				async (c) => {
					c.setCookie("session", "value");
					return { hello: "world" };
				},
			),
			cookieOverride: createAuthEndpoint(
				"/cookie",
				{
					method: "GET",
				},
				async (c) => {
					c.setCookie("data", "1");
				},
			),
			noCookie: createAuthEndpoint(
				"/no-cookie",
				{
					method: "GET",
				},
				async (c) => {},
			),
		};

		const authContext = init({
			hooks: {
				after: createAuthMiddleware(async (c) => {
					c.setHeader("key", "value");
					c.setCookie("data", "2");
				}),
			},
		});

		const authEndpoints = toAuthEndpoints(endpoints, authContext);

		it("set cookies from both hook", async () => {
			const result = await authEndpoints.cookies({
				asResponse: true,
			});
			expect(result.headers.get("set-cookie")).toContain("session=value");
			expect(result.headers.get("set-cookie")).toContain("data=2");
		});

		it("should override cookie", async () => {
			const result = await authEndpoints.cookieOverride({
				asResponse: true,
			});
			expect(result.headers.get("set-cookie")).toContain("data=2");
		});

		it("should only set the hook cookie", async () => {
			const result = await authEndpoints.noCookie({
				asResponse: true,
			});
			expect(result.headers.get("set-cookie")).toContain("data=2");
		});

		it("should return cookies from return headers", async () => {
			const result = await authEndpoints.noCookie({
				returnHeaders: true,
			});
			expect(result.headers.get("set-cookie")).toContain("data=2");

			const result2 = await authEndpoints.cookies({
				asResponse: true,
			});
			expect(result2.headers.get("set-cookie")).toContain("session=value");
			expect(result2.headers.get("set-cookie")).toContain("data=2");
		});
	});
});

describe("disabled paths", async () => {
	const { client } = await getTestInstance({
		disabledPaths: ["/sign-in/email"],
	});

	it("should return 404 for disabled paths", async () => {
		const response = await client.$fetch("/ok");
		expect(response.data).toEqual({ ok: true });
		const { error } = await client.signIn.email({
			email: "[email protected]",
			password: "test",
		});
		expect(error?.status).toBe(404);
	});
});

describe("debug mode stack trace", () => {
	it("should preserve stack trace when logger is in debug mode and APIError is thrown", async () => {
		const endpoints = {
			testEndpoint: createAuthEndpoint(
				"/test-error",
				{ method: "GET" },
				async () => {
					throw new APIError("BAD_REQUEST", { message: "Test error" });
				},
			),
		};

		const authContext = init({
			logger: {
				level: "debug",
			},
		});

		const api = toAuthEndpoints(endpoints, authContext);

		try {
			await api.testEndpoint({});
		} catch (error: any) {
			expect(error).toBeInstanceOf(APIError);
			expect(error.stack).toBeDefined();
			expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
			expect(error.stack).toMatch(/at\s+/);
		}
	});

	it("should not modify stack trace when logger is not in debug mode", async () => {
		const endpoints = {
			testEndpoint: createAuthEndpoint(
				"/test-error",
				{ method: "GET" },
				async () => {
					throw new APIError("BAD_REQUEST", { message: "Test error" });
				},
			),
		};

		const authContext = init({
			logger: {
				level: "error", // Not debug mode
			},
		});

		const api = toAuthEndpoints(endpoints, authContext);

		try {
			await api.testEndpoint({});
		} catch (error: any) {
			expect(error).toBeInstanceOf(APIError);
			// Stack should exist but may be minimal when not in debug mode
			expect(error.stack).toBeDefined();
		}
	});

	it("should have detailed stack trace in debug mode", async () => {
		const endpoints = {
			testEndpoint: createAuthEndpoint(
				"/test-error",
				{ method: "GET" },
				async () => {
					throw new APIError("INTERNAL_SERVER_ERROR", {
						message: "Internal error occurred",
					});
				},
			),
		};

		const authContext = init({
			logger: {
				level: "debug",
			},
		});

		const api = toAuthEndpoints(endpoints, authContext);

		try {
			await api.testEndpoint({});
		} catch (error: any) {
			expect(error).toBeInstanceOf(APIError);
			expect(error.stack).toBeDefined();
			// Check for stack trace format
			expect(error.stack).toMatch(/at\s+.*\(.*\)/); // Match "at functionName (file:line:col)"
			expect(error.stack).toMatch(/\.ts:\d+:\d+/); // Match TypeScript file with line:column
		}
	});

	it("should handle APIError in hooks with debug mode", async () => {
		const endpoints = {
			testEndpoint: createAuthEndpoint(
				"/test-hook-error",
				{ method: "GET" },
				async () => {
					return { data: "success" };
				},
			),
		};

		const authContext = init({
			logger: {
				level: "debug",
			},
			hooks: {
				before: createAuthMiddleware(async () => {
					throw new APIError("FORBIDDEN", { message: "Forbidden action" });
				}),
			},
		});

		const api = toAuthEndpoints(endpoints, authContext);

		try {
			await api.testEndpoint({});
		} catch (error: any) {
			expect(error).toBeInstanceOf(APIError);
			expect(error.stack).toBeDefined();
			expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
			expect(error.stack).toMatch(/at\s+/);
		}
	});

	it("should handle Response containing APIError in debug mode", async () => {
		const endpoints = {
			testEndpoint: createAuthEndpoint(
				"/test-response-error",
				{ method: "GET" },
				async () => {
					throw new APIError("UNAUTHORIZED", {
						message: "Unauthorized access",
					});
				},
			),
		};

		const authContext = init({
			logger: {
				level: "debug",
			},
		});

		const api = toAuthEndpoints(endpoints, authContext);

		// Test with asResponse = true to get Response object
		const response = await api.testEndpoint({ asResponse: true });
		expect(response).toBeInstanceOf(Response);
		expect(response.status).toBe(401);

		// Test with asResponse = false to get thrown error
		try {
			await api.testEndpoint({ asResponse: false });
		} catch (error: any) {
			expect(error).toBeInstanceOf(APIError);
			expect(error.stack).toBeDefined();
			expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
		}
	});
});

```

--------------------------------------------------------------------------------
/packages/expo/src/client.ts:
--------------------------------------------------------------------------------

```typescript
import type { BetterAuthClientPlugin, Store } from "better-auth/types";
import * as Linking from "expo-linking";
import { Platform } from "react-native";
import Constants from "expo-constants";
import type { BetterFetchOption } from "@better-fetch/fetch";

interface CookieAttributes {
	value: string;
	expires?: Date;
	"max-age"?: number;
	domain?: string;
	path?: string;
	secure?: boolean;
	httpOnly?: boolean;
	sameSite?: "Strict" | "Lax" | "None";
}

export function parseSetCookieHeader(
	header: string,
): Map<string, CookieAttributes> {
	const cookieMap = new Map<string, CookieAttributes>();
	const cookies = splitSetCookieHeader(header);
	cookies.forEach((cookie) => {
		const parts = cookie.split(";").map((p) => p.trim());
		const [nameValue, ...attributes] = parts;
		const [name, ...valueParts] = nameValue!.split("=");
		const value = valueParts.join("=");
		const cookieObj: CookieAttributes = { value };
		attributes.forEach((attr) => {
			const [attrName, ...attrValueParts] = attr.split("=");
			const attrValue = attrValueParts.join("=");
			cookieObj[attrName!.toLowerCase() as "value"] = attrValue;
		});
		cookieMap.set(name!, cookieObj);
	});
	return cookieMap;
}

function splitSetCookieHeader(setCookie: string): string[] {
	const parts: string[] = [];
	let buffer = "";
	let i = 0;
	while (i < setCookie.length) {
		const char = setCookie[i];
		if (char === ",") {
			const recent = buffer.toLowerCase();
			const hasExpires = recent.includes("expires=");
			const hasGmt = /gmt/i.test(recent);
			if (hasExpires && !hasGmt) {
				buffer += char;
				i += 1;
				continue;
			}
			if (buffer.trim().length > 0) {
				parts.push(buffer.trim());
				buffer = "";
			}
			i += 1;
			if (setCookie[i] === " ") i += 1;
			continue;
		}
		buffer += char;
		i += 1;
	}
	if (buffer.trim().length > 0) {
		parts.push(buffer.trim());
	}
	return parts;
}

interface ExpoClientOptions {
	scheme?: string;
	storage: {
		setItem: (key: string, value: string) => any;
		getItem: (key: string) => string | null;
	};
	/**
	 * Prefix for local storage keys (e.g., "my-app_cookie", "my-app_session_data")
	 * @default "better-auth"
	 */
	storagePrefix?: string;
	/**
	 * Prefix for server cookie names to filter (e.g., "better-auth.session_token")
	 * This is used to identify which cookies belong to better-auth to prevent
	 * infinite refetching when third-party cookies are set.
	 * @default "better-auth"
	 */
	cookiePrefix?: string;
	disableCache?: boolean;
}

interface StoredCookie {
	value: string;
	expires: string | null;
}

export function getSetCookie(header: string, prevCookie?: string) {
	const parsed = parseSetCookieHeader(header);
	let toSetCookie: Record<string, StoredCookie> = {};
	parsed.forEach((cookie, key) => {
		const expiresAt = cookie["expires"];
		const maxAge = cookie["max-age"];
		const expires = maxAge
			? new Date(Date.now() + Number(maxAge) * 1000)
			: expiresAt
				? new Date(String(expiresAt))
				: null;
		toSetCookie[key] = {
			value: cookie["value"],
			expires: expires ? expires.toISOString() : null,
		};
	});
	if (prevCookie) {
		try {
			const prevCookieParsed = JSON.parse(prevCookie);
			toSetCookie = {
				...prevCookieParsed,
				...toSetCookie,
			};
		} catch {
			//
		}
	}
	return JSON.stringify(toSetCookie);
}

export function getCookie(cookie: string) {
	let parsed = {} as Record<string, StoredCookie>;
	try {
		parsed = JSON.parse(cookie) as Record<string, StoredCookie>;
	} catch (e) {}
	const toSend = Object.entries(parsed).reduce((acc, [key, value]) => {
		if (value.expires && new Date(value.expires) < new Date()) {
			return acc;
		}
		return `${acc}; ${key}=${value.value}`;
	}, "");
	return toSend;
}

function getOrigin(scheme: string) {
	const schemeURI = Linking.createURL("", { scheme });
	return schemeURI;
}

/**
 * Compare if session cookies have actually changed by comparing their values.
 * Ignores expiry timestamps that naturally change on each request.
 *
 * @param prevCookie - Previous cookie JSON string
 * @param newCookie - New cookie JSON string
 * @returns true if session cookies have changed, false otherwise
 */
function hasSessionCookieChanged(
	prevCookie: string | null,
	newCookie: string,
): boolean {
	if (!prevCookie) return true;

	try {
		const prev = JSON.parse(prevCookie) as Record<string, StoredCookie>;
		const next = JSON.parse(newCookie) as Record<string, StoredCookie>;

		// Get all session-related cookie keys (session_token, session_data)
		const sessionKeys = new Set<string>();
		Object.keys(prev).forEach((key) => {
			if (key.includes("session_token") || key.includes("session_data")) {
				sessionKeys.add(key);
			}
		});
		Object.keys(next).forEach((key) => {
			if (key.includes("session_token") || key.includes("session_data")) {
				sessionKeys.add(key);
			}
		});

		// Compare the values of session cookies (ignore expires timestamps)
		for (const key of sessionKeys) {
			const prevValue = prev[key]?.value;
			const nextValue = next[key]?.value;
			if (prevValue !== nextValue) {
				return true;
			}
		}

		return false;
	} catch {
		// If parsing fails, assume cookie changed
		return true;
	}
}

/**
 * Check if the Set-Cookie header contains session-related better-auth cookies.
 * Only triggers session updates when session_token or session_data cookies are present.
 * This prevents infinite refetching when non-session cookies (like third-party cookies) change.
 *
 * Supports multiple cookie naming patterns:
 * - Default: "better-auth.session_token", "__Secure-better-auth.session_token"
 * - Custom prefix: "myapp.session_token", "__Secure-myapp.session_token"
 * - Custom full names: "my_custom_session_token", "custom_session_data"
 * - No prefix (cookiePrefix=""): "session_token", "my_session_token", etc.
 *
 * @param setCookieHeader - The Set-Cookie header value
 * @param cookiePrefix - The cookie prefix to check for. Can be empty string for custom cookie names.
 * @returns true if the header contains session-related cookies, false otherwise
 */
export function hasBetterAuthCookies(
	setCookieHeader: string,
	cookiePrefix: string,
): boolean {
	const cookies = parseSetCookieHeader(setCookieHeader);
	const sessionCookieSuffixes = ["session_token", "session_data"];

	// Check if any cookie is a session-related cookie
	for (const name of cookies.keys()) {
		// Remove __Secure- prefix if present for comparison
		const nameWithoutSecure = name.startsWith("__Secure-")
			? name.slice(9)
			: name;

		for (const suffix of sessionCookieSuffixes) {
			if (cookiePrefix) {
				// When prefix is provided, only match exact pattern: "prefix.suffix"
				if (nameWithoutSecure === `${cookiePrefix}.${suffix}`) {
					return true;
				}
			} else {
				// When prefix is empty, check for:
				// 1. Exact match: "session_token"
				// 2. Custom names ending with suffix: "my_custom_session_token"
				if (nameWithoutSecure.endsWith(suffix)) {
					return true;
				}
			}
		}
	}
	return false;
}

/**
 * Expo secure store does not support colons in the keys.
 * This function replaces colons with underscores.
 *
 * @see https://github.com/better-auth/better-auth/issues/5426
 *
 * @param name cookie name to be saved in the storage
 * @returns normalized cookie name
 */
export function normalizeCookieName(name: string) {
	return name.replace(/:/g, "_");
}

export function storageAdapter(storage: {
	getItem: (name: string) => string | null;
	setItem: (name: string, value: string) => void;
}) {
	return {
		getItem: (name: string) => {
			return storage.getItem(normalizeCookieName(name));
		},
		setItem: (name: string, value: string) => {
			return storage.setItem(normalizeCookieName(name), value);
		},
	};
}

export const expoClient = (opts: ExpoClientOptions) => {
	let store: Store | null = null;
	const storagePrefix = opts?.storagePrefix || "better-auth";
	const cookieName = `${storagePrefix}_cookie`;
	const localCacheName = `${storagePrefix}_session_data`;
	const storage = storageAdapter(opts?.storage);
	const isWeb = Platform.OS === "web";
	const cookiePrefix = opts?.cookiePrefix || "better-auth";

	const rawScheme =
		opts?.scheme || Constants.expoConfig?.scheme || Constants.platform?.scheme;
	const scheme = Array.isArray(rawScheme) ? rawScheme[0] : rawScheme;

	if (!scheme && !isWeb) {
		throw new Error(
			"Scheme not found in app.json. Please provide a scheme in the options.",
		);
	}
	return {
		id: "expo",
		getActions(_, $store) {
			store = $store;
			return {
				/**
				 * Get the stored cookie.
				 *
				 * You can use this to get the cookie stored in the device and use it in your fetch
				 * requests.
				 *
				 * @example
				 * ```ts
				 * const cookie = client.getCookie();
				 * fetch("https://api.example.com", {
				 * 	headers: {
				 * 		cookie,
				 * 	},
				 * });
				 */
				getCookie: () => {
					const cookie = storage.getItem(cookieName);
					return getCookie(cookie || "{}");
				},
			};
		},
		fetchPlugins: [
			{
				id: "expo",
				name: "Expo",
				hooks: {
					async onSuccess(context) {
						if (isWeb) return;
						const setCookie = context.response.headers.get("set-cookie");
						if (setCookie) {
							// Only process and notify if the Set-Cookie header contains better-auth cookies
							// This prevents infinite refetching when other cookies (like Cloudflare's __cf_bm) are present
							if (hasBetterAuthCookies(setCookie, cookiePrefix)) {
								const prevCookie = await storage.getItem(cookieName);
								const toSetCookie = getSetCookie(
									setCookie || "",
									prevCookie ?? undefined,
								);
								// Only notify $sessionSignal if the session cookie values actually changed
								// This prevents infinite refetching when the server sends the same cookie with updated expiry
								if (hasSessionCookieChanged(prevCookie, toSetCookie)) {
									await storage.setItem(cookieName, toSetCookie);
									store?.notify("$sessionSignal");
								} else {
									// Still update the storage to refresh expiry times, but don't trigger refetch
									await storage.setItem(cookieName, toSetCookie);
								}
							}
						}

						if (
							context.request.url.toString().includes("/get-session") &&
							!opts?.disableCache
						) {
							const data = context.data;
							storage.setItem(localCacheName, JSON.stringify(data));
						}

						if (
							context.data?.redirect &&
							(context.request.url.toString().includes("/sign-in") ||
								context.request.url.toString().includes("/link-social")) &&
							!context.request?.body.includes("idToken") // id token is used for silent sign-in
						) {
							const callbackURL = JSON.parse(context.request.body)?.callbackURL;
							const to = callbackURL;
							const signInURL = context.data?.url;
							let Browser: typeof import("expo-web-browser") | undefined =
								undefined;
							try {
								Browser = await import("expo-web-browser");
							} catch (error) {
								throw new Error(
									'"expo-web-browser" is not installed as a dependency!',
									{
										cause: error,
									},
								);
							}
							const proxyURL = `${context.request.baseURL}/expo-authorization-proxy?authorizationURL=${encodeURIComponent(signInURL)}`;
							const result = await Browser.openAuthSessionAsync(proxyURL, to);
							if (result.type !== "success") return;
							const url = new URL(result.url);
							const cookie = String(url.searchParams.get("cookie"));
							if (!cookie) return;
							storage.setItem(cookieName, getSetCookie(cookie));
							store?.notify("$sessionSignal");
						}
					},
				},
				async init(url, options) {
					if (isWeb) {
						return {
							url,
							options: options as BetterFetchOption,
						};
					}
					options = options || {};
					const storedCookie = storage.getItem(cookieName);
					const cookie = getCookie(storedCookie || "{}");
					options.credentials = "omit";
					options.headers = {
						...options.headers,
						cookie,
						"expo-origin": getOrigin(scheme!),
						"x-skip-oauth-proxy": "true", // skip oauth proxy for expo
					};
					if (options.body?.callbackURL) {
						if (options.body.callbackURL.startsWith("/")) {
							const url = Linking.createURL(options.body.callbackURL, {
								scheme,
							});
							options.body.callbackURL = url;
						}
					}
					if (options.body?.newUserCallbackURL) {
						if (options.body.newUserCallbackURL.startsWith("/")) {
							const url = Linking.createURL(options.body.newUserCallbackURL, {
								scheme,
							});
							options.body.newUserCallbackURL = url;
						}
					}
					if (options.body?.errorCallbackURL) {
						if (options.body.errorCallbackURL.startsWith("/")) {
							const url = Linking.createURL(options.body.errorCallbackURL, {
								scheme,
							});
							options.body.errorCallbackURL = url;
						}
					}
					if (url.includes("/sign-out")) {
						await storage.setItem(cookieName, "{}");
						store?.atoms.session?.set({
							...store.atoms.session.get(),
							data: null,
							error: null,
							isPending: false,
						});
						storage.setItem(localCacheName, "{}");
					}
					return {
						url,
						options: options as BetterFetchOption,
					};
				},
			},
		],
	} satisfies BetterAuthClientPlugin;
};

```

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

```typescript
import { describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { $deviceAuthorizationOptionsSchema, deviceAuthorization } from ".";
import { deviceAuthorizationClient } from "./client";
import type { DeviceCode } from "./schema";

describe("device authorization plugin input validation", () => {
	it("basic validation", async () => {
		const options = $deviceAuthorizationOptionsSchema.parse({});
		expect(options).toMatchInlineSnapshot(`
			{
			  "deviceCodeLength": 40,
			  "expiresIn": "30m",
			  "interval": "5s",
			  "userCodeLength": 8,
			}
		`);
	});

	it("should validate custom options", async () => {
		const options = $deviceAuthorizationOptionsSchema.parse({
			expiresIn: 60 * 1000,
			interval: 2 * 1000,
			deviceCodeLength: 50,
			userCodeLength: 10,
		});
		expect(options).toMatchInlineSnapshot(`
			{
			  "deviceCodeLength": 50,
			  "expiresIn": 60000,
			  "interval": 2000,
			  "userCodeLength": 10,
			}
		`);
	});
});

describe("client validation", async () => {
	const validClients = ["valid-client-1", "valid-client-2"];

	const { auth } = await getTestInstance({
		plugins: [
			deviceAuthorization({
				validateClient: async (clientId) => {
					return validClients.includes(clientId);
				},
			}),
		],
	});

	it("should reject invalid client in device code request", async () => {
		await expect(
			auth.api.deviceCode({
				body: {
					client_id: "invalid-client",
				},
			}),
		).rejects.toMatchObject({
			body: {
				error: "invalid_client",
				error_description: "Invalid client ID",
			},
		});
	});

	it("should accept valid client in device code request", async () => {
		const response = await auth.api.deviceCode({
			body: {
				client_id: "valid-client-1",
			},
		});
		expect(response.device_code).toBeDefined();
	});

	it("should reject invalid client in token request", async () => {
		const { device_code } = await auth.api.deviceCode({
			body: {
				client_id: "valid-client-1",
			},
		});

		await expect(
			auth.api.deviceToken({
				body: {
					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
					device_code,
					client_id: "invalid-client",
				},
			}),
		).rejects.toMatchObject({
			body: {
				error: "invalid_grant",
				error_description: "Invalid client ID",
			},
		});
	});

	it("should reject mismatched client_id in token request", async () => {
		const { device_code } = await auth.api.deviceCode({
			body: {
				client_id: "valid-client-1",
			},
		});

		await expect(
			auth.api.deviceToken({
				body: {
					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
					device_code,
					client_id: "valid-client-2",
				},
			}),
		).rejects.toMatchObject({
			body: {
				error: "invalid_grant",
				error_description: "Client ID mismatch",
			},
		});
	});
});

describe("device authorization flow", async () => {
	const { auth, client, sessionSetter, signInWithTestUser } =
		await getTestInstance(
			{
				plugins: [
					deviceAuthorization({
						expiresIn: "5min",
						interval: "2s",
					}),
				],
			},
			{
				clientOptions: {
					plugins: [deviceAuthorizationClient()],
				},
			},
		);

	describe("device code request", () => {
		it("should generate device and user codes", async () => {
			const response = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			expect(response.device_code).toBeDefined();
			expect(response.user_code).toBeDefined();
			expect(response.verification_uri).toBeDefined();
			expect(response.verification_uri_complete).toBeDefined();
			expect(response.expires_in).toBe(300);
			expect(response.interval).toBe(2);
			expect(response.user_code).toMatch(/^[A-Z0-9]{8}$/);
			expect(response.verification_uri_complete).toContain(response.user_code);
		});

		it("should support custom client ID and scope", async () => {
			const response = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
					scope: "read write",
				},
			});

			expect(response.device_code).toBeDefined();
			expect(response.user_code).toBeDefined();
		});
	});

	describe("device token polling", () => {
		it("should return authorization_pending when not approved", async () => {
			const { device_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			await expect(
				auth.api.deviceToken({
					body: {
						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
						device_code: device_code,
						client_id: "test-client",
					},
				}),
			).rejects.toMatchObject({
				body: {
					error: "authorization_pending",
					error_description: "Authorization pending",
				},
			});
		});

		it("should return expired_token for expired device codes", async () => {
			const { device_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			// Advance time past expiration
			vi.useFakeTimers();
			await vi.advanceTimersByTimeAsync(301 * 1000); // 301 seconds

			await expect(
				auth.api.deviceToken({
					body: {
						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
						device_code: device_code,
						client_id: "test-client",
					},
				}),
			).rejects.toMatchObject({
				body: {
					error: "expired_token",
					error_description: "Device code has expired",
				},
			});

			vi.useRealTimers();
		});

		it("should return error for invalid device code", async () => {
			await expect(
				auth.api.deviceToken({
					body: {
						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
						device_code: "invalid-code",
						client_id: "test-client",
					},
				}),
			).rejects.toMatchObject({
				body: {
					error: "invalid_grant",
					error_description: "Invalid device code",
				},
			});
		});
	});

	describe("device verification", () => {
		it("should verify valid user code", async () => {
			const { user_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			const response = await auth.api.deviceVerify({
				query: { user_code },
			});
			expect("error" in response).toBe(false);
			if (!("error" in response)) {
				expect(response.user_code).toBe(user_code);
				expect(response.status).toBe("pending");
			}
		});

		it("should handle invalid user code", async () => {
			await expect(
				auth.api.deviceVerify({
					query: { user_code: "INVALID" },
				}),
			).rejects.toMatchObject({
				body: {
					error: "invalid_request",
					error_description: "Invalid user code",
				},
			});
		});
	});

	describe("device approval flow", () => {
		it("should approve device and create session", async () => {
			// First, sign in as a user
			const { headers } = await signInWithTestUser();

			// Request device code
			const { device_code, user_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			// Approve the device
			const approveResponse = await auth.api.deviceApprove({
				body: { userCode: user_code },
				headers,
			});
			expect("success" in approveResponse && approveResponse.success).toBe(
				true,
			);

			// Poll for token should now succeed
			const tokenResponse = await auth.api.deviceToken({
				body: {
					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
					device_code: device_code,
					client_id: "test-client",
				},
			});
			// Check OAuth 2.0 compliant response
			expect("access_token" in tokenResponse).toBe(true);
			if ("access_token" in tokenResponse) {
				expect(tokenResponse.access_token).toBeDefined();
				expect(tokenResponse.token_type).toBe("Bearer");
				expect(tokenResponse.expires_in).toBeGreaterThan(0);
				expect(tokenResponse.scope).toBeDefined();
			}
		});

		it("should deny device authorization", async () => {
			const { device_code, user_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			// Deny the device
			const denyResponse = await auth.api.deviceDeny({
				body: { userCode: user_code },
				headers: new Headers(),
			});
			expect("success" in denyResponse && denyResponse.success).toBe(true);

			// Poll for token should return access_denied
			await expect(
				auth.api.deviceToken({
					body: {
						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
						device_code: device_code,
						client_id: "test-client",
					},
				}),
			).rejects.toMatchObject({
				body: {
					error: "access_denied",
					error_description: "Access denied",
				},
			});
		});

		it("should require authentication for approval", async () => {
			const { user_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			await expect(
				auth.api.deviceApprove({
					body: { userCode: user_code },
					headers: new Headers(),
				}),
			).rejects.toMatchObject({
				body: {
					error: "unauthorized",
					error_description: "Authentication required",
				},
			});
		});

		it("should enforce rate limiting with slow_down error", async () => {
			const { device_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});

			await auth.api
				.deviceToken({
					body: {
						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
						device_code: device_code,
						client_id: "test-client",
					},
				})
				.catch(
					// ignore the error
					() => {},
				);

			await expect(
				auth.api.deviceToken({
					body: {
						grant_type: "urn:ietf:params:oauth:grant-type:device_code",
						device_code: device_code,
						client_id: "test-client",
					},
				}),
			).rejects.toMatchObject({
				body: {
					error: "slow_down",
					error_description: "Polling too frequently",
				},
			});
		});
	});

	describe("edge cases", () => {
		it("should not allow approving already processed device code", async () => {
			// Sign in as a user
			const { headers } = await signInWithTestUser();

			// Request and approve device
			const { user_code: userCode } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});
			await auth.api.deviceApprove({
				body: { userCode },
				headers,
			});

			await expect(
				auth.api.deviceApprove({
					body: { userCode },
					headers,
				}),
			).rejects.toMatchObject({
				body: {
					error: "invalid_request",
					error_description: "Device code already processed",
				},
			});
		});

		it("should handle user code without dashes", async () => {
			const { user_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
				},
			});
			const cleanUserCode = user_code.replace(/-/g, "");

			const response = await auth.api.deviceVerify({
				query: { user_code: cleanUserCode },
			});
			expect("status" in response && response.status).toBe("pending");
		});

		it("should store and use scope from device code request", async () => {
			const { headers } = await signInWithTestUser();

			const { device_code, user_code } = await auth.api.deviceCode({
				body: {
					client_id: "test-client",
					scope: "read write profile",
				},
			});

			await auth.api.deviceApprove({
				body: { userCode: user_code },
				headers,
			});

			const tokenResponse = await auth.api.deviceToken({
				body: {
					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
					device_code: device_code,
					client_id: "test-client",
				},
			});
			expect("scope" in tokenResponse && tokenResponse.scope).toBe(
				"read write profile",
			);
		});
	});
});

describe("device authorization with custom options", async () => {
	it("should correctly store interval as milliseconds in database", async () => {
		const { auth, client, db } = await getTestInstance({
			plugins: [
				deviceAuthorization({
					interval: "5s",
				}),
			],
		});

		const response = await auth.api.deviceCode({
			body: {
				client_id: "test-client",
			},
		});

		// Response should return interval in seconds
		expect(response.interval).toBe(5);

		// Check that the interval is stored as milliseconds in the database
		const deviceCodeRecord: DeviceCode | null = await db.findOne({
			model: "deviceCode",
			where: [
				{
					field: "deviceCode",
					value: response.device_code,
				},
			],
		});

		// Should be stored as 5000 milliseconds, not "5s" string
		expect(deviceCodeRecord?.pollingInterval).toBe(5000);
		expect(typeof deviceCodeRecord?.pollingInterval).toBe("number");
	});

	it("should use custom code generators", async () => {
		const customDeviceCode = "custom-device-code-12345";
		const customUserCode = "CUSTOM12";

		const { auth } = await getTestInstance({
			plugins: [
				deviceAuthorization({
					generateDeviceCode: () => customDeviceCode,
					generateUserCode: () => customUserCode,
				}),
			],
		});

		const response = await auth.api.deviceCode({
			body: {
				client_id: "test-client",
			},
		});
		expect(response.device_code).toBe(customDeviceCode);
		expect(response.user_code).toBe(customUserCode);
	});

	it("should respect custom expiration time", async () => {
		const { auth } = await getTestInstance({
			plugins: [
				deviceAuthorization({
					expiresIn: "1min",
				}),
			],
		});

		const response = await auth.api.deviceCode({
			body: {
				client_id: "test-client",
			},
		});
		expect(response.expires_in).toBe(60);
	});
});

```

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

```typescript
import { generateRandomString } from "../../../crypto/random";
import * as z from "zod";
import { createAuthEndpoint } from "@better-auth/core/api";
import { sessionMiddleware } from "../../../api";
import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
import type {
	TwoFactorProvider,
	TwoFactorTable,
	UserWithTwoFactor,
} from "../types";
import { APIError } from "better-call";
import { TWO_FACTOR_ERROR_CODES } from "../error-code";
import { verifyTwoFactor } from "../verify-two-factor";
import { safeJSONParse } from "../../../utils/json";

export interface BackupCodeOptions {
	/**
	 * The amount of backup codes to generate
	 *
	 * @default 10
	 */
	amount?: number;
	/**
	 * The length of the backup codes
	 *
	 * @default 10
	 */
	length?: number;
	/**
	 * An optional custom function to generate backup codes
	 */
	customBackupCodesGenerate?: () => string[];
	/**
	 * How to store the backup codes in the database, whether encrypted or plain.
	 */
	storeBackupCodes?:
		| "plain"
		| "encrypted"
		| {
				encrypt: (token: string) => Promise<string>;
				decrypt: (token: string) => Promise<string>;
		  };
}

function generateBackupCodesFn(options?: BackupCodeOptions) {
	return Array.from({ length: options?.amount ?? 10 })
		.fill(null)
		.map(() => generateRandomString(options?.length ?? 10, "a-z", "0-9", "A-Z"))
		.map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
}

export async function generateBackupCodes(
	secret: string,
	options?: BackupCodeOptions,
) {
	const backupCodes = options?.customBackupCodesGenerate
		? options.customBackupCodesGenerate()
		: generateBackupCodesFn(options);
	if (options?.storeBackupCodes === "encrypted") {
		const encCodes = await symmetricEncrypt({
			data: JSON.stringify(backupCodes),
			key: secret,
		});
		return {
			backupCodes,
			encryptedBackupCodes: encCodes,
		};
	}
	if (
		typeof options?.storeBackupCodes === "object" &&
		"encrypt" in options?.storeBackupCodes
	) {
		return {
			backupCodes,
			encryptedBackupCodes: await options?.storeBackupCodes.encrypt(
				JSON.stringify(backupCodes),
			),
		};
	}
	return {
		backupCodes,
		encryptedBackupCodes: JSON.stringify(backupCodes),
	};
}

export async function verifyBackupCode(
	data: {
		backupCodes: string;
		code: string;
	},
	key: string,
	options?: BackupCodeOptions,
) {
	const codes = await getBackupCodes(data.backupCodes, key, options);
	if (!codes) {
		return {
			status: false,
			updated: null,
		};
	}
	return {
		status: codes.includes(data.code),
		updated: codes.filter((code) => code !== data.code),
	};
}

export async function getBackupCodes(
	backupCodes: string,
	key: string,
	options?: BackupCodeOptions,
) {
	if (options?.storeBackupCodes === "encrypted") {
		const decrypted = await symmetricDecrypt({ key, data: backupCodes });
		return safeJSONParse<string[]>(decrypted);
	}
	if (
		typeof options?.storeBackupCodes === "object" &&
		"decrypt" in options?.storeBackupCodes
	) {
		const decrypted = await options?.storeBackupCodes.decrypt(backupCodes);
		return safeJSONParse<string[]>(decrypted);
	}

	return safeJSONParse<string[]>(backupCodes);
}

export const backupCode2fa = (opts: BackupCodeOptions) => {
	const twoFactorTable = "twoFactor";

	return {
		id: "backup_code",
		endpoints: {
			/**
			 * ### Endpoint
			 *
			 * POST `/two-factor/verify-backup-code`
			 *
			 * ### API Methods
			 *
			 * **server:**
			 * `auth.api.verifyBackupCode`
			 *
			 * **client:**
			 * `authClient.twoFactor.verifyBackupCode`
			 *
			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-verify-backup-code)
			 */
			verifyBackupCode: createAuthEndpoint(
				"/two-factor/verify-backup-code",

				{
					method: "POST",
					body: z.object({
						code: z.string().meta({
							description: `A backup code to verify. Eg: "123456"`,
						}),
						/**
						 * Disable setting the session cookie
						 */
						disableSession: z
							.boolean()
							.meta({
								description: "If true, the session cookie will not be set.",
							})
							.optional(),
						/**
						 * if true, the device will be trusted
						 * for 30 days. It'll be refreshed on
						 * every sign in request within this time.
						 */
						trustDevice: z
							.boolean()
							.meta({
								description:
									"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
							})
							.optional(),
					}),
					metadata: {
						openapi: {
							description: "Verify a backup code for two-factor authentication",
							responses: {
								"200": {
									description: "Backup code verified successfully",
									content: {
										"application/json": {
											schema: {
												type: "object",
												properties: {
													user: {
														type: "object",
														properties: {
															id: {
																type: "string",
																description: "Unique identifier of the user",
															},
															email: {
																type: "string",
																format: "email",
																nullable: true,
																description: "User's email address",
															},
															emailVerified: {
																type: "boolean",
																nullable: true,
																description: "Whether the email is verified",
															},
															name: {
																type: "string",
																nullable: true,
																description: "User's name",
															},
															image: {
																type: "string",
																format: "uri",
																nullable: true,
																description: "User's profile image URL",
															},
															twoFactorEnabled: {
																type: "boolean",
																description:
																	"Whether two-factor authentication is enabled for the user",
															},
															createdAt: {
																type: "string",
																format: "date-time",
																description:
																	"Timestamp when the user was created",
															},
															updatedAt: {
																type: "string",
																format: "date-time",
																description:
																	"Timestamp when the user was last updated",
															},
														},
														required: [
															"id",
															"twoFactorEnabled",
															"createdAt",
															"updatedAt",
														],
														description:
															"The authenticated user object with two-factor details",
													},
													session: {
														type: "object",
														properties: {
															token: {
																type: "string",
																description: "Session token",
															},
															userId: {
																type: "string",
																description:
																	"ID of the user associated with the session",
															},
															createdAt: {
																type: "string",
																format: "date-time",
																description:
																	"Timestamp when the session was created",
															},
															expiresAt: {
																type: "string",
																format: "date-time",
																description:
																	"Timestamp when the session expires",
															},
														},
														required: [
															"token",
															"userId",
															"createdAt",
															"expiresAt",
														],
														description:
															"The current session object, included unless disableSession is true",
													},
												},
												required: ["user", "session"],
											},
										},
									},
								},
							},
						},
					},
				},
				async (ctx) => {
					const { session, valid } = await verifyTwoFactor(ctx);
					const user = session.user as UserWithTwoFactor;
					const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({
						model: twoFactorTable,
						where: [
							{
								field: "userId",
								value: user.id,
							},
						],
					});
					if (!twoFactor) {
						throw new APIError("BAD_REQUEST", {
							message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
						});
					}
					const validate = await verifyBackupCode(
						{
							backupCodes: twoFactor.backupCodes,
							code: ctx.body.code,
						},
						ctx.context.secret,
						opts,
					);
					if (!validate.status) {
						throw new APIError("UNAUTHORIZED", {
							message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
						});
					}
					const updatedBackupCodes = await symmetricEncrypt({
						key: ctx.context.secret,
						data: JSON.stringify(validate.updated),
					});

					await ctx.context.adapter.updateMany({
						model: twoFactorTable,
						update: {
							backupCodes: updatedBackupCodes,
						},
						where: [
							{
								field: "userId",
								value: user.id,
							},
						],
					});

					if (!ctx.body.disableSession) {
						return valid(ctx);
					}
					return ctx.json({
						token: session.session?.token,
						user: {
							id: session.user?.id,
							email: session.user.email,
							emailVerified: session.user.emailVerified,
							name: session.user.name,
							image: session.user.image,
							createdAt: session.user.createdAt,
							updatedAt: session.user.updatedAt,
						},
					});
				},
			),
			/**
			 * ### Endpoint
			 *
			 * POST `/two-factor/generate-backup-codes`
			 *
			 * ### API Methods
			 *
			 * **server:**
			 * `auth.api.generateBackupCodes`
			 *
			 * **client:**
			 * `authClient.twoFactor.generateBackupCodes`
			 *
			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-generate-backup-codes)
			 */
			generateBackupCodes: createAuthEndpoint(
				"/two-factor/generate-backup-codes",
				{
					method: "POST",
					body: z.object({
						password: z.string().meta({
							description: "The users password.",
						}),
					}),
					use: [sessionMiddleware],
					metadata: {
						openapi: {
							description:
								"Generate new backup codes for two-factor authentication",
							responses: {
								"200": {
									description: "Backup codes generated successfully",
									content: {
										"application/json": {
											schema: {
												type: "object",
												properties: {
													status: {
														type: "boolean",
														description:
															"Indicates if the backup codes were generated successfully",
														enum: [true],
													},
													backupCodes: {
														type: "array",
														items: { type: "string" },
														description:
															"Array of generated backup codes in plain text",
													},
												},
												required: ["status", "backupCodes"],
											},
										},
									},
								},
							},
						},
					},
				},
				async (ctx) => {
					const user = ctx.context.session.user as UserWithTwoFactor;
					if (!user.twoFactorEnabled) {
						throw new APIError("BAD_REQUEST", {
							message: TWO_FACTOR_ERROR_CODES.TWO_FACTOR_NOT_ENABLED,
						});
					}
					await ctx.context.password.checkPassword(user.id, ctx);
					const backupCodes = await generateBackupCodes(
						ctx.context.secret,
						opts,
					);
					await ctx.context.adapter.updateMany({
						model: twoFactorTable,
						update: {
							backupCodes: backupCodes.encryptedBackupCodes,
						},
						where: [
							{
								field: "userId",
								value: ctx.context.session.user.id,
							},
						],
					});
					return ctx.json({
						status: true,
						backupCodes: backupCodes.backupCodes,
					});
				},
			),
			/**
			 * ### Endpoint
			 *
			 * GET `/two-factor/view-backup-codes`
			 *
			 * ### API Methods
			 *
			 * **server:**
			 * `auth.api.viewBackupCodes`
			 *
			 * **client:**
			 * `authClient.twoFactor.viewBackupCodes`
			 *
			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-view-backup-codes)
			 */
			viewBackupCodes: createAuthEndpoint(
				"/two-factor/view-backup-codes",
				{
					method: "GET",
					body: z.object({
						userId: z.coerce.string().meta({
							description: `The user ID to view all backup codes. Eg: "user-id"`,
						}),
					}),
					metadata: {
						SERVER_ONLY: true,
					},
				},
				async (ctx) => {
					const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({
						model: twoFactorTable,
						where: [
							{
								field: "userId",
								value: ctx.body.userId,
							},
						],
					});
					if (!twoFactor) {
						throw new APIError("BAD_REQUEST", {
							message: TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED,
						});
					}
					const decryptedBackupCodes = await getBackupCodes(
						twoFactor.backupCodes,
						ctx.context.secret,
						opts,
					);

					if (!decryptedBackupCodes) {
						throw new APIError("BAD_REQUEST", {
							message: TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE,
						});
					}
					return ctx.json({
						status: true,
						backupCodes: decryptedBackupCodes,
					});
				},
			),
		},
	} satisfies TwoFactorProvider;
};

```

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

```typescript
import {
	and,
	asc,
	count,
	desc,
	eq,
	gt,
	gte,
	inArray,
	notInArray,
	like,
	lt,
	lte,
	ne,
	or,
	sql,
	SQL,
} from "drizzle-orm";
import { BetterAuthError } from "@better-auth/core/error";
import type { BetterAuthOptions } from "@better-auth/core";
import {
	createAdapterFactory,
	type AdapterFactoryOptions,
	type AdapterFactoryCustomizeAdapterCreator,
} from "../adapter-factory";
import type {
	DBAdapterDebugLogOption,
	DBAdapter,
	Where,
} from "@better-auth/core/db/adapter";

export interface DB {
	[key: string]: any;
}

export interface DrizzleAdapterConfig {
	/**
	 * The schema object that defines the tables and fields
	 */
	schema?: Record<string, any>;
	/**
	 * The database provider
	 */
	provider: "pg" | "mysql" | "sqlite";
	/**
	 * If the table names in the schema are plural
	 * set this to true. For example, if the schema
	 * has an object with a key "users" instead of "user"
	 */
	usePlural?: boolean;
	/**
	 * Enable debug logs for the adapter
	 *
	 * @default false
	 */
	debugLogs?: DBAdapterDebugLogOption;
	/**
	 * By default snake case is used for table and field names
	 * when the CLI is used to generate the schema. If you want
	 * to use camel case, set this to true.
	 * @default false
	 */
	camelCase?: boolean;
	/**
	 * Whether to execute multiple operations in a transaction.
	 *
	 * If the database doesn't support transactions,
	 * set this to `false` and operations will be executed sequentially.
	 * @default false
	 */
	transaction?: boolean;
}

export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
	let lazyOptions: BetterAuthOptions | null = null;
	const createCustomAdapter =
		(db: DB): AdapterFactoryCustomizeAdapterCreator =>
		({ getFieldName, debugLog }) => {
			function getSchema(model: string) {
				const schema = config.schema || db._.fullSchema;
				if (!schema) {
					throw new BetterAuthError(
						"Drizzle adapter failed to initialize. Schema not found. Please provide a schema object in the adapter options object.",
					);
				}
				const schemaModel = schema[model];
				if (!schemaModel) {
					throw new BetterAuthError(
						`[# Drizzle Adapter]: The model "${model}" was not found in the schema object. Please pass the schema directly to the adapter options.`,
					);
				}
				return schemaModel;
			}
			const withReturning = async (
				model: string,
				builder: any,
				data: Record<string, any>,
				where?: Where[],
			) => {
				if (config.provider !== "mysql") {
					const c = await builder.returning();
					return c[0];
				}
				await builder.execute();
				const schemaModel = getSchema(model);
				const builderVal = builder.config?.values;
				if (where?.length) {
					// If we're updating a field that's in the where clause, use the new value
					const updatedWhere = where.map((w) => {
						// If this field was updated, use the new value for lookup
						if (data[w.field] !== undefined) {
							return { ...w, value: data[w.field] };
						}
						return w;
					});

					const clause = convertWhereClause(updatedWhere, model);
					const res = await db
						.select()
						.from(schemaModel)
						.where(...clause);
					return res[0];
				} else if (builderVal && builderVal[0]?.id?.value) {
					let tId = builderVal[0]?.id?.value;
					if (!tId) {
						//get last inserted id
						const lastInsertId = await db
							.select({ id: sql`LAST_INSERT_ID()` })
							.from(schemaModel)
							.orderBy(desc(schemaModel.id))
							.limit(1);
						tId = lastInsertId[0].id;
					}
					const res = await db
						.select()
						.from(schemaModel)
						.where(eq(schemaModel.id, tId))
						.limit(1)
						.execute();
					return res[0];
				} else if (data.id) {
					const res = await db
						.select()
						.from(schemaModel)
						.where(eq(schemaModel.id, data.id))
						.limit(1)
						.execute();
					return res[0];
				} else {
					// If the user doesn't have `id` as a field, then this will fail.
					// We expect that they defined `id` in all of their models.
					if (!("id" in schemaModel)) {
						throw new BetterAuthError(
							`The model "${model}" does not have an "id" field. Please use the "id" field as your primary key.`,
						);
					}
					const res = await db
						.select()
						.from(schemaModel)
						.orderBy(desc(schemaModel.id))
						.limit(1)
						.execute();
					return res[0];
				}
			};
			function convertWhereClause(where: Where[], model: string) {
				const schemaModel = getSchema(model);
				if (!where) return [];
				if (where.length === 1) {
					const w = where[0];
					if (!w) {
						return [];
					}
					const field = getFieldName({ model, field: w.field });
					if (!schemaModel[field]) {
						throw new BetterAuthError(
							`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`,
						);
					}
					if (w.operator === "in") {
						if (!Array.isArray(w.value)) {
							throw new BetterAuthError(
								`The value for the field "${w.field}" must be an array when using the "in" operator.`,
							);
						}
						return [inArray(schemaModel[field], w.value)];
					}

					if (w.operator === "not_in") {
						if (!Array.isArray(w.value)) {
							throw new BetterAuthError(
								`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
							);
						}
						return [notInArray(schemaModel[field], w.value)];
					}

					if (w.operator === "contains") {
						return [like(schemaModel[field], `%${w.value}%`)];
					}

					if (w.operator === "starts_with") {
						return [like(schemaModel[field], `${w.value}%`)];
					}

					if (w.operator === "ends_with") {
						return [like(schemaModel[field], `%${w.value}`)];
					}

					if (w.operator === "lt") {
						return [lt(schemaModel[field], w.value)];
					}

					if (w.operator === "lte") {
						return [lte(schemaModel[field], w.value)];
					}

					if (w.operator === "ne") {
						return [ne(schemaModel[field], w.value)];
					}

					if (w.operator === "gt") {
						return [gt(schemaModel[field], w.value)];
					}

					if (w.operator === "gte") {
						return [gte(schemaModel[field], w.value)];
					}

					return [eq(schemaModel[field], w.value)];
				}
				const andGroup = where.filter(
					(w) => w.connector === "AND" || !w.connector,
				);
				const orGroup = where.filter((w) => w.connector === "OR");

				const andClause = and(
					...andGroup.map((w) => {
						const field = getFieldName({ model, field: w.field });
						if (w.operator === "in") {
							if (!Array.isArray(w.value)) {
								throw new BetterAuthError(
									`The value for the field "${w.field}" must be an array when using the "in" operator.`,
								);
							}
							return inArray(schemaModel[field], w.value);
						}
						if (w.operator === "not_in") {
							if (!Array.isArray(w.value)) {
								throw new BetterAuthError(
									`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
								);
							}
							return notInArray(schemaModel[field], w.value);
						}
						if (w.operator === "contains") {
							return like(schemaModel[field], `%${w.value}%`);
						}
						if (w.operator === "starts_with") {
							return like(schemaModel[field], `${w.value}%`);
						}
						if (w.operator === "ends_with") {
							return like(schemaModel[field], `%${w.value}`);
						}
						if (w.operator === "lt") {
							return lt(schemaModel[field], w.value);
						}
						if (w.operator === "lte") {
							return lte(schemaModel[field], w.value);
						}
						if (w.operator === "gt") {
							return gt(schemaModel[field], w.value);
						}
						if (w.operator === "gte") {
							return gte(schemaModel[field], w.value);
						}
						if (w.operator === "ne") {
							return ne(schemaModel[field], w.value);
						}
						return eq(schemaModel[field], w.value);
					}),
				);
				const orClause = or(
					...orGroup.map((w) => {
						const field = getFieldName({ model, field: w.field });
						if (w.operator === "in") {
							if (!Array.isArray(w.value)) {
								throw new BetterAuthError(
									`The value for the field "${w.field}" must be an array when using the "in" operator.`,
								);
							}
							return inArray(schemaModel[field], w.value);
						}
						if (w.operator === "not_in") {
							if (!Array.isArray(w.value)) {
								throw new BetterAuthError(
									`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
								);
							}
							return notInArray(schemaModel[field], w.value);
						}
						if (w.operator === "contains") {
							return like(schemaModel[field], `%${w.value}%`);
						}
						if (w.operator === "starts_with") {
							return like(schemaModel[field], `${w.value}%`);
						}
						if (w.operator === "ends_with") {
							return like(schemaModel[field], `%${w.value}`);
						}
						if (w.operator === "lt") {
							return lt(schemaModel[field], w.value);
						}
						if (w.operator === "lte") {
							return lte(schemaModel[field], w.value);
						}
						if (w.operator === "gt") {
							return gt(schemaModel[field], w.value);
						}
						if (w.operator === "gte") {
							return gte(schemaModel[field], w.value);
						}
						if (w.operator === "ne") {
							return ne(schemaModel[field], w.value);
						}
						return eq(schemaModel[field], w.value);
					}),
				);

				const clause: SQL<unknown>[] = [];

				if (andGroup.length) clause.push(andClause!);
				if (orGroup.length) clause.push(orClause!);
				return clause;
			}
			function checkMissingFields(
				schema: Record<string, any>,
				model: string,
				values: Record<string, any>,
			) {
				if (!schema) {
					throw new BetterAuthError(
						"Drizzle adapter failed to initialize. Schema not found. Please provide a schema object in the adapter options object.",
					);
				}
				for (const key in values) {
					if (!schema[key]) {
						throw new BetterAuthError(
							`The field "${key}" does not exist in the "${model}" schema. Please update your drizzle schema or re-generate using "npx @better-auth/cli generate".`,
						);
					}
				}
			}
			return {
				async create({ model, data: values }) {
					const schemaModel = getSchema(model);
					checkMissingFields(schemaModel, model, values);
					const builder = db.insert(schemaModel).values(values);
					const returned = await withReturning(model, builder, values);
					return returned;
				},
				async findOne({ model, where }) {
					const schemaModel = getSchema(model);
					const clause = convertWhereClause(where, model);
					const res = await db
						.select()
						.from(schemaModel)
						.where(...clause);
					if (!res.length) return null;
					return res[0];
				},
				async findMany({ model, where, sortBy, limit, offset }) {
					const schemaModel = getSchema(model);
					const clause = where ? convertWhereClause(where, model) : [];

					const sortFn = sortBy?.direction === "desc" ? desc : asc;
					const builder = db
						.select()
						.from(schemaModel)
						.limit(limit || 100)
						.offset(offset || 0);
					if (sortBy?.field) {
						builder.orderBy(
							sortFn(
								schemaModel[getFieldName({ model, field: sortBy?.field })],
							),
						);
					}
					return (await builder.where(...clause)) as any[];
				},
				async count({ model, where }) {
					const schemaModel = getSchema(model);
					const clause = where ? convertWhereClause(where, model) : [];
					const res = await db
						.select({ count: count() })
						.from(schemaModel)
						.where(...clause);
					return res[0].count;
				},
				async update({ model, where, update: values }) {
					const schemaModel = getSchema(model);
					const clause = convertWhereClause(where, model);
					const builder = db
						.update(schemaModel)
						.set(values)
						.where(...clause);
					return await withReturning(model, builder, values as any, where);
				},
				async updateMany({ model, where, update: values }) {
					const schemaModel = getSchema(model);
					const clause = convertWhereClause(where, model);
					const builder = db
						.update(schemaModel)
						.set(values)
						.where(...clause);
					return await builder;
				},
				async delete({ model, where }) {
					const schemaModel = getSchema(model);
					const clause = convertWhereClause(where, model);
					const builder = db.delete(schemaModel).where(...clause);
					return await builder;
				},
				async deleteMany({ model, where }) {
					const schemaModel = getSchema(model);
					const clause = convertWhereClause(where, model);
					const builder = db.delete(schemaModel).where(...clause);
					return await builder;
				},
				options: config,
			};
		};
	let adapterOptions: AdapterFactoryOptions | null = null;
	adapterOptions = {
		config: {
			adapterId: "drizzle",
			adapterName: "Drizzle Adapter",
			usePlural: config.usePlural ?? false,
			debugLogs: config.debugLogs ?? false,
			transaction:
				(config.transaction ?? false)
					? (cb) =>
							db.transaction((tx: DB) => {
								const adapter = createAdapterFactory({
									config: adapterOptions!.config,
									adapter: createCustomAdapter(tx),
								})(lazyOptions!);
								return cb(adapter);
							})
					: false,
		},
		adapter: createCustomAdapter(db),
	};
	const adapter = createAdapterFactory(adapterOptions);
	return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
		lazyOptions = options;
		return adapter(options);
	};
};

```

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

```typescript
import { describe, expect } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { createAuthClient } from "../../client";
import { createAuthEndpoint } from "@better-auth/core/api";
import { isSimpleRequest, originCheck } from "./origin-check";
import * as z from "zod";

describe("Origin Check", async (it) => {
	const { customFetchImpl, testUser } = await getTestInstance({
		trustedOrigins: [
			"http://localhost:5000",
			"https://trusted.com",
			"*.my-site.com",
			"https://*.protocol-site.com",
		],
		emailAndPassword: {
			enabled: true,
			async sendResetPassword(url, user) {},
		},
		advanced: {
			disableCSRFCheck: false,
			disableOriginCheck: false,
		},
	});

	it("should allow trusted origins", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "http://localhost:3000",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "http://localhost:3000/callback",
		});
		expect(res.data?.user).toBeDefined();
	});

	it("should not allow untrusted origins", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
			},
		});
		const res = await client.signIn.email({
			email: "[email protected]",
			password: "password",
			callbackURL: "http://malicious.com",
		});
		expect(res.error?.status).toBe(403);
		expect(res.error?.message).toBe("Invalid callbackURL");
	});

	it("should allow query params in callback url", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://localhost:3000",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "/dashboard?test=123",
		});
		expect(res.data?.user).toBeDefined();
	});

	it("should allow plus signs in the callback url", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://localhost:3000",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "/dashboard+page?test=123+456",
		});
		expect(res.data?.user).toBeDefined();
	});

	it("should reject callback url with double slash", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://localhost:3000",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "//evil.com",
		});
		expect(res.error?.status).toBe(403);
	});

	it("should reject callback urls with encoded malicious content", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://localhost:3000",
				},
			},
		});

		const maliciousPatterns = [
			"/%5C/evil.com",
			`/\\/\\/evil.com`,
			"/%5C/evil.com",
			"/..%2F..%2Fevil.com",
			"javascript:alert('xss')",
			"data:text/html,<script>alert('xss')</script>",
		];

		for (const pattern of maliciousPatterns) {
			const res = await client.signIn.email({
				email: testUser.email,
				password: testUser.password,
				callbackURL: pattern,
			});
			expect(res.error?.status).toBe(403);
		}
	});

	it("should reject untrusted origin headers", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "malicious.com",
					cookie: "session=123",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(res.error?.status).toBe(403);
	});

	it("should reject untrusted origin headers which start with trusted origin", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://trusted.com.malicious.com",
					cookie: "session=123",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(res.error?.status).toBe(403);
	});

	it("should reject untrusted origin subdomains", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "http://sub-domain.trusted.com",
					cookie: "session=123",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(res.error?.status).toBe(403);
	});

	it("should allow untrusted origin if they don't contain cookies", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "http://sub-domain.trusted.com",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(res.data?.user).toBeDefined();
	});

	it("should reject untrusted redirectTo", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
			},
		});
		const res = await client.requestPasswordReset({
			email: testUser.email,
			redirectTo: "http://malicious.com",
		});
		expect(res.error?.status).toBe(403);
		expect(res.error?.message).toBe("Invalid redirectURL");
	});

	it("should work with list of trusted origins", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://trusted.com",
				},
			},
		});
		const res = await client.requestPasswordReset({
			email: testUser.email,
			redirectTo: "http://localhost:5000/reset-password",
		});
		expect(res.data?.status).toBeTruthy();

		const res2 = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			fetchOptions: {
				query: {
					currentURL: "http://localhost:5000",
				},
			},
		});
		expect(res2.data?.user).toBeDefined();
	});

	it("should work with wildcard trusted origins", async (ctx) => {
		const client = createAuthClient({
			baseURL: "https://sub-domain.my-site.com",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://sub-domain.my-site.com",
				},
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "https://sub-domain.my-site.com/callback",
		});
		expect(res.data?.user).toBeDefined();

		// Test another subdomain with the wildcard pattern
		const client2 = createAuthClient({
			baseURL: "https://another-sub.my-site.com",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://another-sub.my-site.com",
				},
			},
		});
		const res2 = await client2.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "https://another-sub.my-site.com/callback",
		});
		expect(res2.data?.user).toBeDefined();
	});

	it("should work with GET requests", async (ctx) => {
		const client = createAuthClient({
			baseURL: "https://sub-domain.my-site.com",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://google.com",
					cookie: "value",
				},
			},
		});
		const res = await client.$fetch("/ok");
		expect(res.data).toMatchObject({ ok: true });
	});

	it("should handle POST requests with proper origin validation", async (ctx) => {
		// Test with valid origin
		const validClient = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "http://localhost:5000",
					cookie: "session=123",
				},
			},
		});
		const validRes = await validClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(validRes.data?.user).toBeDefined();

		// Test with invalid origin
		const invalidClient = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "http://untrusted-domain.com",
					cookie: "session=123",
				},
			},
		});
		const invalidRes = await invalidClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(invalidRes.error?.status).toBe(403);
	});

	it("should work with relative callbackURL with query params", async (ctx) => {
		const client = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
			},
		});
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "/[email protected]",
		});
		expect(res.data?.user).toBeDefined();
	});

	it("should work with protocol specific wildcard trusted origins", async () => {
		// Test HTTPS protocol specific wildcard - should work
		const httpsClient = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "https://api.protocol-site.com",
					cookie: "session=123",
				},
			},
		});
		const httpsRes = await httpsClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "https://app.protocol-site.com/dashboard",
		});
		expect(httpsRes.data?.user).toBeDefined();

		// Test HTTP with HTTPS protocol wildcard - should fail
		const httpClient = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
				headers: {
					origin: "http://api.protocol-site.com",
					cookie: "session=123",
				},
			},
		});
		const httpRes = await httpClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(httpRes.error?.status).toBe(403);
	});

	it("should work with custom scheme wildcards (e.g. exp:// for Expo)", async () => {
		const { customFetchImpl, testUser } = await getTestInstance({
			trustedOrigins: [
				"exp://10.0.0.*:*/*",
				"exp://192.168.*.*:*/*",
				"exp://172.*.*.*:*/*",
			],
			emailAndPassword: {
				enabled: true,
				async sendResetPassword(url, user) {},
			},
		});

		// Test custom scheme with wildcard - should work
		const expoClient = createAuthClient({
			baseURL: "http://localhost:3000",
			fetchOptions: {
				customFetchImpl,
			},
		});

		// Test with IP matching the wildcard pattern
		const resWithIP = await expoClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "exp://10.0.0.29:8081/--/",
		});
		expect(resWithIP.data?.user).toBeDefined();

		// Test with different IP range that matches
		const resWithIP2 = await expoClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "exp://192.168.1.100:8081/--/",
		});
		expect(resWithIP2.data?.user).toBeDefined();

		// Test with different IP range that matches
		const resWithIP3 = await expoClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "exp://172.16.0.1:8081/--/",
		});
		expect(resWithIP3.data?.user).toBeDefined();

		// Test with IP that doesn't match any pattern - should fail
		const resWithUnmatchedIP = await expoClient.signIn.email({
			email: testUser.email,
			password: testUser.password,
			callbackURL: "exp://203.0.113.0:8081/--/",
		});
		expect(resWithUnmatchedIP.error?.status).toBe(403);
	});
});

describe("origin check middleware", async (it) => {
	it("should return invalid origin", async () => {
		const { client } = await getTestInstance({
			trustedOrigins: ["https://trusted-site.com"],
			plugins: [
				{
					id: "test",
					endpoints: {
						test: createAuthEndpoint(
							"/test",
							{
								method: "GET",
								query: z.object({
									callbackURL: z.string(),
								}),
								use: [originCheck((c) => c.query.callbackURL)],
							},
							async (c) => {
								return c.query.callbackURL;
							},
						),
					},
				},
			],
		});
		const invalid = await client.$fetch(
			"/test?callbackURL=https://malicious-site.com",
		);
		expect(invalid.error?.status).toBe(403);
		const valid = await client.$fetch("/test?callbackURL=/dashboard");
		expect(valid.data).toBe("/dashboard");
		const validTrusted = await client.$fetch(
			"/test?callbackURL=https://trusted-site.com/path",
		);
		expect(validTrusted.data).toBe("https://trusted-site.com/path");

		const sampleInternalEndpointInvalid = await client.$fetch(
			"/verify-email?callbackURL=https://malicious-site.com&token=xyz",
		);
		expect(sampleInternalEndpointInvalid.error?.status).toBe(403);
	});
});

describe("is simple request", async (it) => {
	it("should return true for simple requests", async () => {
		const request = new Request("http://localhost:3000/test", {
			method: "GET",
		});
		const isSimple = isSimpleRequest(request.headers);
		expect(isSimple).toBe(true);
	});

	it("should return false for non-simple requests", async () => {
		const request = new Request("http://localhost:3000/test", {
			method: "POST",
			headers: {
				"custom-header": "value",
			},
		});
		const isSimple = isSimpleRequest(request.headers);
		expect(isSimple).toBe(false);
	});

	it("should return false for requests with a content type that is not simple", async () => {
		const request = new Request("http://localhost:3000/test", {
			method: "POST",
			headers: {
				"content-type": "application/json",
			},
		});
		const isSimple = isSimpleRequest(request.headers);
		expect(isSimple).toBe(false);
	});

	it;
});

```

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

```markdown
---
title: Waku Integration
description: Integrate Better Auth with Waku.
---

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).

## Create auth instance

Create a file named `auth.ts` in your application. Import Better Auth and create your instance.

<Callout type="warn">
Make sure to export the auth instance with the variable name `auth` or as a `default` export.
</Callout>

```ts title="src/auth.ts"
import { betterAuth } from "better-auth"

export const auth = betterAuth({
    database: {
        provider: "postgres", //change this to your database provider
        url: process.env.DATABASE_URL, // path to your database or connection string
    }
})
```

## Create API Route

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:

```ts title="src/pages/api/auth/[...route].ts"
import { auth } from "../../../auth" // Adjust the path as necessary

export const GET = async (request: Request): Promise<Response> => {
  return auth.handler(request)
}

export const POST = async (request: Request): Promise<Response> => {
  return auth.handler(request)
}
```

<Callout type="info">
 You can change the path on your better-auth configuration but it's recommended to keep it as `src/pages/api/auth/[...route].ts`
</Callout>

## Create a client

Create a client instance. Here we are creating `auth-client.ts` file inside the `lib/` directory.

```ts title="src/lib/auth-client.ts"
import { createAuthClient } from "better-auth/react" // make sure to import from better-auth/react

export const authClient = createAuthClient({
    //you can pass client configuration here
})

export type Session = typeof authClient.$Infer.Session // you can infer typescript types from the authClient
```

Once you have created the client, you can use it to sign up, sign in, and perform other actions.
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.

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.

## RSC and Server actions

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.

**Example: Getting Session on a server action**

```tsx title="server.ts"
"use server" // Waku currently only supports file-level "use server"

import { auth } from "./auth"
import { getContext } from "waku/middleware/context"

export const someAuthenticatedAction = async () => {
  "use server"
  const session = await auth.api.getSession({
    headers: new Headers(getContext().req.headers),
  })
};
```

**Example: Getting Session on a RSC**


```tsx
import { auth } from "../auth"
import { getContext } from "waku/middleware/context"

export async function ServerComponent() {
    const session = await auth.api.getSession({
        headers: new Headers(getContext().req.headers),
    })
    if(!session) {
        return <div>Not authenticated</div>
    }
    return (
        <div>
            <h1>Welcome {session.user.name}</h1>
        </div>
    )
}
```

<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>

### Server Action Cookies

When you call a function that needs to set cookies, like `signInEmail` or `signUpEmail` in a server action, cookies won’t be set.

We can create a plugin that works together with our middleware to set cookies.

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { wakuCookies } from "better-auth/waku";
import { getContextData } from "waku/middleware/context";

export const auth = betterAuth({
    //...your config
    plugins: [wakuCookies()] // make sure this is the last plugin in the array // [!code highlight]
})

function wakuCookies() {
  return {
    id: "waku-cookies",
    hooks: {
      after: [
        {
          matcher(ctx) {
            return true;
          },
          handler: createAuthMiddleware(async (ctx) => {
            const returned = ctx.context.responseHeaders;
            if ("_flag" in ctx && ctx._flag === "router") {
              return;
            }
            if (returned instanceof Headers) {
              const setCookieHeader = returned?.get("set-cookie");
              if (!setCookieHeader) return;
              const contextData = getContextData();
              contextData.betterAuthSetCookie = setCookieHeader;
            }
          }),
        },
      ],
    },
  } satisfies BetterAuthPlugin;
}
```

See below for the middleware to create to add the `contextData.betterAuthSetCookie` cookies to the response.
Now, when you call functions that set cookies, they will be automatically set.

```ts
"use server";
import { auth } from "../auth"

const signIn = async () => {
    await auth.api.signInEmail({
        body: {
            email: "[email protected]",
            password: "password",
        }
    })
}
```

### Middleware

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.

You can use the `getSessionCookie` helper from Better Auth for this purpose:

<Callout type="warn">
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>.
</Callout>

```ts title="src/middleware/auth.ts"
import type { Middleware } from "waku/config"
import { getSession } from "../auth"
import { getSessionCookie } from "better-auth/cookies"

const authMiddleware: Middleware = () => {
    return async (ctx, next) => {
        const sessionCookie = getSessionCookie(
            new Request(ctx.req.url, {
                body: ctx.req.body,
                headers: ctx.req.headers,
                method: ctx.req.method,
            })
        )
        // THIS IS NOT SECURE!
        // This is the recommended approach to optimistically redirect users
        // We recommend handling auth checks in each page/route
        if (!sessionCookie && ctx.req.url.pathname !== "/") {
            if (!ctx.req.url.pathname.endsWith(".txt")) {
                // Currently RSC requests end in .txt and don't handle redirect responses
                // The redirect needs to be encoded in the React flight stream somehow
                // There is some functionality in Waku to do this from a server component
                // but not from middleware.
                ctx.res.status = 302;
                ctx.res.headers = {
                  Location: new URL("/", ctx.req.url).toString(),
                };
            }
        }

        // TODO possible to inspect ctx.req.url and not do this on every request
        // Or skip starting the promise here and just invoke from server components and functions
        getSession()
        await next()
        if (ctx.data.betterAuthSetCookie) {
            ctx.res.headers ||= {}
            let origSetCookie = ctx.res.headers["set-cookie"] || ([] as string[])
            if (typeof origSetCookie === "string") {
                origSetCookie = [origSetCookie]
            }
            ctx.res.headers["set-cookie"] = [
                ...origSetCookie,
                ctx.data.betterAuthSetCookie as string,
            ]
        }
    }
};

export default authMiddleware;
```

<Callout type="warn">
	**Security Warning:** The `getSessionCookie` function only checks for the
	existence of a session cookie; it does **not** validate it. Relying solely
	on this check for security is dangerous, as anyone can manually create a
	cookie to bypass it. You must always validate the session on your server for
	any protected actions or pages.
</Callout>

<Callout type="info">
If you have a custom cookie name or prefix, you can pass it to the `getSessionCookie` function.
```ts
const sessionCookie = getSessionCookie(request, {
    cookieName: "my_session_cookie",
    cookiePrefix: "my_prefix"
})
```
</Callout>

Alternatively, you can use the `getCookieCache` helper to get the session object from the cookie cache.

```ts
import { getCookieCache } from "better-auth/cookies"

const authMiddleware: Middleware = () => {
    return async (ctx, next) => {
        const session = await getCookieCache(ctx.req)
        if (!session && ctx.req.url.pathname !== "/") {
            if (!ctx.req.url.pathname.endsWith(".txt")) {
                ctx.res.status = 302
                ctx.res.headers = {
                    Location: new URL("/", ctx.req.url).toString(),
                }
            }
        }
    }
    await next();
  }
}

export default authMiddleware;
```

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):

```ts title="waku.config.ts"
import { defineConfig } from "waku/config";

export default defineConfig({
  middleware: [
    "waku/middleware/context",
    "waku/middleware/dev-server",
    "./src/middleware/auth.ts",
    "waku/middleware/handler",
  ],
});
```

### How to handle auth checks in each page/route

In this example, we are using the `auth.api.getSession` function within a server component to get the session object,
then we are checking if the session is valid. If it's not, we are redirecting the user to the sign-in page.
Waku has `getContext` to get the request headers and `getContextData()` to store data per request. We can use this
to avoid fetching the session more than once per request.

```ts title="auth.ts"
import { getContext, getContextData } from "waku/middleware/context";

// Code from above to create the server auth config
// export const auth = ...

export function getSession(): Promise<Session | null> {
  const contextData = getContextData();
  const ctx = getContext();
  const existingSessionPromise = contextData.sessionPromise as
    | Promise<Session | null>
    | undefined;
  if (existingSessionPromise) {
    return existingSessionPromise;
  }
  const sessionPromise = auth.api.getSession({
    headers: new Headers(ctx.req.headers),
  });
  contextData.sessionPromise = sessionPromise;
  return sessionPromise;
}
```


```tsx title="src/pages/dashboard.tsx"
import { getSession } from "../auth";
import { unstable_redirect as redirect } from 'waku/router/server';

export default async function DashboardPage() {
    const session = await getSession()

    if (!session) {
        redirect("/sign-in")
    }

    return (
        <div>
            <h1>Welcome {session.user.name}</h1>
        </div>
    )
}
```

### Example usage

#### Sign Up

```ts title="src/components/signup.tsx"
"use client"

import { useState } from "react"
import { authClient } from "../lib/auth-client"

export default function SignUp() {
  const [email, setEmail] = useState("")
  const [name, setName] = useState("")
  const [password, setPassword] = useState("")

  const signUp = async () => {
    await authClient.signUp.email(
      {
        email,
        password,
        name,
      },
      {
        onRequest: (ctx) => {
          // show loading state
        },
        onSuccess: (ctx) => {
          // redirect to home
        },
        onError: (ctx) => {
          alert(ctx.error)
        },
      },
    )
  }

  return (
    <div>
      <h2>
        Sign Up
      </h2>
      <form
        onSubmit={signUp}
      >
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Name"
        />
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
        />
        <button
          type="submit"
        >
          Sign Up
        </button>
      </form>
    </div>
  )
}

```

#### Sign In

```ts title="src/components/signin.tsx"
"use client"

import { useState } from "react"
import { authClient } from "../lib/auth-client"

export default function SignIn() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")

  const signIn = async () => {
    await authClient.signIn.email(
      {
        email,
        password,
      },
      {
        onRequest: (ctx) => {
          // show loading state
        },
        onSuccess: (ctx) => {
          // redirect to home
        },
        onError: (ctx) => {
          alert(ctx.error)
        },
      },
    )
  }

  return (
    <div>
      <h2>
        Sign In
      </h2>
      <form onSubmit={signIn}>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <button
          type="submit"
        >
          Sign In
        </button>
      </form>
    </div>
  )
}
```

```
Page 25/51FirstPrevNextLast