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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/demo/nextjs/app/admin/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import { useQuery, useQueryClient } from "@tanstack/react-query";
  4 | import { format } from "date-fns";
  5 | import {
  6 | 	Calendar as CalendarIcon,
  7 | 	Loader2,
  8 | 	Plus,
  9 | 	RefreshCw,
 10 | 	Trash,
 11 | 	UserCircle,
 12 | } from "lucide-react";
 13 | import { useRouter } from "next/navigation";
 14 | import { useState } from "react";
 15 | import { Toaster, toast } from "sonner";
 16 | import { Badge } from "@/components/ui/badge";
 17 | import { Button } from "@/components/ui/button";
 18 | import { Calendar } from "@/components/ui/calendar";
 19 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 20 | import {
 21 | 	Dialog,
 22 | 	DialogContent,
 23 | 	DialogHeader,
 24 | 	DialogTitle,
 25 | 	DialogTrigger,
 26 | } from "@/components/ui/dialog";
 27 | import { Input } from "@/components/ui/input";
 28 | import { Label } from "@/components/ui/label";
 29 | import {
 30 | 	Popover,
 31 | 	PopoverContent,
 32 | 	PopoverTrigger,
 33 | } from "@/components/ui/popover";
 34 | import {
 35 | 	Select,
 36 | 	SelectContent,
 37 | 	SelectItem,
 38 | 	SelectTrigger,
 39 | 	SelectValue,
 40 | } from "@/components/ui/select";
 41 | import {
 42 | 	Table,
 43 | 	TableBody,
 44 | 	TableCell,
 45 | 	TableHead,
 46 | 	TableHeader,
 47 | 	TableRow,
 48 | } from "@/components/ui/table";
 49 | import { client } from "@/lib/auth-client";
 50 | import { cn } from "@/lib/utils";
 51 | 
 52 | type User = {
 53 | 	id: string;
 54 | 	email: string;
 55 | 	name: string;
 56 | 	role: "admin" | "user";
 57 | };
 58 | 
 59 | export default function AdminDashboard() {
 60 | 	const queryClient = useQueryClient();
 61 | 	const router = useRouter();
 62 | 	const [isDialogOpen, setIsDialogOpen] = useState(false);
 63 | 	const [newUser, setNewUser] = useState({
 64 | 		email: "",
 65 | 		password: "",
 66 | 		name: "",
 67 | 		role: "user" as const,
 68 | 	});
 69 | 	const [isLoading, setIsLoading] = useState<string | undefined>();
 70 | 	const [isBanDialogOpen, setIsBanDialogOpen] = useState(false);
 71 | 	const [banForm, setBanForm] = useState({
 72 | 		userId: "",
 73 | 		reason: "",
 74 | 		expirationDate: undefined as Date | undefined,
 75 | 	});
 76 | 
 77 | 	const { data: users, isLoading: isUsersLoading } = useQuery({
 78 | 		queryKey: ["users"],
 79 | 		queryFn: async () => {
 80 | 			const data = await client.admin.listUsers(
 81 | 				{
 82 | 					query: {
 83 | 						limit: 10,
 84 | 						sortBy: "createdAt",
 85 | 						sortDirection: "desc",
 86 | 					},
 87 | 				},
 88 | 				{
 89 | 					throw: true,
 90 | 				},
 91 | 			);
 92 | 			return data?.users || [];
 93 | 		},
 94 | 	});
 95 | 
 96 | 	const handleCreateUser = async (e: React.FormEvent) => {
 97 | 		e.preventDefault();
 98 | 		setIsLoading("create");
 99 | 		try {
100 | 			await client.admin.createUser({
101 | 				email: newUser.email,
102 | 				password: newUser.password,
103 | 				name: newUser.name,
104 | 				role: newUser.role,
105 | 			});
106 | 			toast.success("User created successfully");
107 | 			setNewUser({ email: "", password: "", name: "", role: "user" });
108 | 			setIsDialogOpen(false);
109 | 			queryClient.invalidateQueries({
110 | 				queryKey: ["users"],
111 | 			});
112 | 		} catch (error: any) {
113 | 			toast.error(error.message || "Failed to create user");
114 | 		} finally {
115 | 			setIsLoading(undefined);
116 | 		}
117 | 	};
118 | 
119 | 	const handleDeleteUser = async (id: string) => {
120 | 		setIsLoading(`delete-${id}`);
121 | 		try {
122 | 			await client.admin.removeUser({ userId: id });
123 | 			toast.success("User deleted successfully");
124 | 			queryClient.invalidateQueries({
125 | 				queryKey: ["users"],
126 | 			});
127 | 		} catch (error: any) {
128 | 			toast.error(error.message || "Failed to delete user");
129 | 		} finally {
130 | 			setIsLoading(undefined);
131 | 		}
132 | 	};
133 | 
134 | 	const handleRevokeSessions = async (id: string) => {
135 | 		setIsLoading(`revoke-${id}`);
136 | 		try {
137 | 			await client.admin.revokeUserSessions({ userId: id });
138 | 			toast.success("Sessions revoked for user");
139 | 		} catch (error: any) {
140 | 			toast.error(error.message || "Failed to revoke sessions");
141 | 		} finally {
142 | 			setIsLoading(undefined);
143 | 		}
144 | 	};
145 | 
146 | 	const handleImpersonateUser = async (id: string) => {
147 | 		setIsLoading(`impersonate-${id}`);
148 | 		try {
149 | 			await client.admin.impersonateUser({ userId: id });
150 | 			toast.success("Impersonated user");
151 | 			router.push("/dashboard");
152 | 		} catch (error: any) {
153 | 			toast.error(error.message || "Failed to impersonate user");
154 | 		} finally {
155 | 			setIsLoading(undefined);
156 | 		}
157 | 	};
158 | 
159 | 	const handleBanUser = async (e: React.FormEvent) => {
160 | 		e.preventDefault();
161 | 		setIsLoading(`ban-${banForm.userId}`);
162 | 		try {
163 | 			if (!banForm.expirationDate) {
164 | 				throw new Error("Expiration date is required");
165 | 			}
166 | 			await client.admin.banUser({
167 | 				userId: banForm.userId,
168 | 				banReason: banForm.reason,
169 | 				banExpiresIn: banForm.expirationDate.getTime() - new Date().getTime(),
170 | 			});
171 | 			toast.success("User banned successfully");
172 | 			setIsBanDialogOpen(false);
173 | 			queryClient.invalidateQueries({
174 | 				queryKey: ["users"],
175 | 			});
176 | 		} catch (error: any) {
177 | 			toast.error(error.message || "Failed to ban user");
178 | 		} finally {
179 | 			setIsLoading(undefined);
180 | 		}
181 | 	};
182 | 
183 | 	return (
184 | 		<div className="container mx-auto p-4 space-y-8">
185 | 			<Toaster richColors />
186 | 			<Card>
187 | 				<CardHeader className="flex flex-row items-center justify-between">
188 | 					<CardTitle className="text-2xl">Admin Dashboard</CardTitle>
189 | 					<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
190 | 						<DialogTrigger asChild>
191 | 							<Button>
192 | 								<Plus className="mr-2 h-4 w-4" /> Create User
193 | 							</Button>
194 | 						</DialogTrigger>
195 | 						<DialogContent>
196 | 							<DialogHeader>
197 | 								<DialogTitle>Create New User</DialogTitle>
198 | 							</DialogHeader>
199 | 							<form onSubmit={handleCreateUser} className="space-y-4">
200 | 								<div>
201 | 									<Label htmlFor="email">Email</Label>
202 | 									<Input
203 | 										id="email"
204 | 										type="email"
205 | 										value={newUser.email}
206 | 										onChange={(e) =>
207 | 											setNewUser({ ...newUser, email: e.target.value })
208 | 										}
209 | 										required
210 | 									/>
211 | 								</div>
212 | 								<div>
213 | 									<Label htmlFor="password">Password</Label>
214 | 									<Input
215 | 										id="password"
216 | 										type="password"
217 | 										value={newUser.password}
218 | 										onChange={(e) =>
219 | 											setNewUser({ ...newUser, password: e.target.value })
220 | 										}
221 | 										required
222 | 									/>
223 | 								</div>
224 | 								<div>
225 | 									<Label htmlFor="name">Name</Label>
226 | 									<Input
227 | 										id="name"
228 | 										value={newUser.name}
229 | 										onChange={(e) =>
230 | 											setNewUser({ ...newUser, name: e.target.value })
231 | 										}
232 | 										required
233 | 									/>
234 | 								</div>
235 | 								<div>
236 | 									<Label htmlFor="role">Role</Label>
237 | 									<Select
238 | 										value={newUser.role}
239 | 										onValueChange={(value: "admin" | "user") =>
240 | 											setNewUser({ ...newUser, role: value as "user" })
241 | 										}
242 | 									>
243 | 										<SelectTrigger>
244 | 											<SelectValue placeholder="Select role" />
245 | 										</SelectTrigger>
246 | 										<SelectContent>
247 | 											<SelectItem value="admin">Admin</SelectItem>
248 | 											<SelectItem value="user">User</SelectItem>
249 | 										</SelectContent>
250 | 									</Select>
251 | 								</div>
252 | 								<Button
253 | 									type="submit"
254 | 									className="w-full"
255 | 									disabled={isLoading === "create"}
256 | 								>
257 | 									{isLoading === "create" ? (
258 | 										<>
259 | 											<Loader2 className="mr-2 h-4 w-4 animate-spin" />
260 | 											Creating...
261 | 										</>
262 | 									) : (
263 | 										"Create User"
264 | 									)}
265 | 								</Button>
266 | 							</form>
267 | 						</DialogContent>
268 | 					</Dialog>
269 | 					<Dialog open={isBanDialogOpen} onOpenChange={setIsBanDialogOpen}>
270 | 						<DialogContent>
271 | 							<DialogHeader>
272 | 								<DialogTitle>Ban User</DialogTitle>
273 | 							</DialogHeader>
274 | 							<form onSubmit={handleBanUser} className="space-y-4">
275 | 								<div>
276 | 									<Label htmlFor="reason">Reason</Label>
277 | 									<Input
278 | 										id="reason"
279 | 										value={banForm.reason}
280 | 										onChange={(e) =>
281 | 											setBanForm({ ...banForm, reason: e.target.value })
282 | 										}
283 | 										required
284 | 									/>
285 | 								</div>
286 | 								<div className="flex flex-col space-y-1.5">
287 | 									<Label htmlFor="expirationDate">Expiration Date</Label>
288 | 									<Popover>
289 | 										<PopoverTrigger asChild>
290 | 											<Button
291 | 												id="expirationDate"
292 | 												variant={"outline"}
293 | 												className={cn(
294 | 													"w-full justify-start text-left font-normal",
295 | 													!banForm.expirationDate && "text-muted-foreground",
296 | 												)}
297 | 											>
298 | 												<CalendarIcon className="mr-2 h-4 w-4" />
299 | 												{banForm.expirationDate ? (
300 | 													format(banForm.expirationDate, "PPP")
301 | 												) : (
302 | 													<span>Pick a date</span>
303 | 												)}
304 | 											</Button>
305 | 										</PopoverTrigger>
306 | 										<PopoverContent className="w-auto p-0">
307 | 											<Calendar
308 | 												mode="single"
309 | 												selected={banForm.expirationDate}
310 | 												onSelect={(date) =>
311 | 													setBanForm({ ...banForm, expirationDate: date })
312 | 												}
313 | 												initialFocus
314 | 											/>
315 | 										</PopoverContent>
316 | 									</Popover>
317 | 								</div>
318 | 								<Button
319 | 									type="submit"
320 | 									className="w-full"
321 | 									disabled={isLoading === `ban-${banForm.userId}`}
322 | 								>
323 | 									{isLoading === `ban-${banForm.userId}` ? (
324 | 										<>
325 | 											<Loader2 className="mr-2 h-4 w-4 animate-spin" />
326 | 											Banning...
327 | 										</>
328 | 									) : (
329 | 										"Ban User"
330 | 									)}
331 | 								</Button>
332 | 							</form>
333 | 						</DialogContent>
334 | 					</Dialog>
335 | 				</CardHeader>
336 | 				<CardContent>
337 | 					{isUsersLoading ? (
338 | 						<div className="flex justify-center items-center h-64">
339 | 							<Loader2 className="h-8 w-8 animate-spin" />
340 | 						</div>
341 | 					) : (
342 | 						<Table>
343 | 							<TableHeader>
344 | 								<TableRow>
345 | 									<TableHead>Email</TableHead>
346 | 									<TableHead>Name</TableHead>
347 | 									<TableHead>Role</TableHead>
348 | 									<TableHead>Banned</TableHead>
349 | 									<TableHead>Actions</TableHead>
350 | 								</TableRow>
351 | 							</TableHeader>
352 | 							<TableBody>
353 | 								{users?.map((user) => (
354 | 									<TableRow key={user.id}>
355 | 										<TableCell>{user.email}</TableCell>
356 | 										<TableCell>{user.name}</TableCell>
357 | 										<TableCell>{user.role || "user"}</TableCell>
358 | 										<TableCell>
359 | 											{user.banned ? (
360 | 												<Badge variant="destructive">Yes</Badge>
361 | 											) : (
362 | 												<Badge variant="outline">No</Badge>
363 | 											)}
364 | 										</TableCell>
365 | 										<TableCell>
366 | 											<div className="flex space-x-2">
367 | 												<Button
368 | 													variant="destructive"
369 | 													size="sm"
370 | 													onClick={() => handleDeleteUser(user.id)}
371 | 													disabled={isLoading?.startsWith("delete")}
372 | 												>
373 | 													{isLoading === `delete-${user.id}` ? (
374 | 														<Loader2 className="h-4 w-4 animate-spin" />
375 | 													) : (
376 | 														<Trash className="h-4 w-4" />
377 | 													)}
378 | 												</Button>
379 | 												<Button
380 | 													variant="outline"
381 | 													size="sm"
382 | 													onClick={() => handleRevokeSessions(user.id)}
383 | 													disabled={isLoading?.startsWith("revoke")}
384 | 												>
385 | 													{isLoading === `revoke-${user.id}` ? (
386 | 														<Loader2 className="h-4 w-4 animate-spin" />
387 | 													) : (
388 | 														<RefreshCw className="h-4 w-4" />
389 | 													)}
390 | 												</Button>
391 | 												<Button
392 | 													variant="secondary"
393 | 													size="sm"
394 | 													onClick={() => handleImpersonateUser(user.id)}
395 | 													disabled={isLoading?.startsWith("impersonate")}
396 | 												>
397 | 													{isLoading === `impersonate-${user.id}` ? (
398 | 														<Loader2 className="h-4 w-4 animate-spin" />
399 | 													) : (
400 | 														<>
401 | 															<UserCircle className="h-4 w-4 mr-2" />
402 | 															Impersonate
403 | 														</>
404 | 													)}
405 | 												</Button>
406 | 												<Button
407 | 													variant="outline"
408 | 													size="sm"
409 | 													onClick={async () => {
410 | 														setBanForm({
411 | 															userId: user.id,
412 | 															reason: "",
413 | 															expirationDate: undefined,
414 | 														});
415 | 														if (user.banned) {
416 | 															setIsLoading(`ban-${user.id}`);
417 | 															await client.admin.unbanUser(
418 | 																{
419 | 																	userId: user.id,
420 | 																},
421 | 																{
422 | 																	onError(context) {
423 | 																		toast.error(
424 | 																			context.error.message ||
425 | 																				"Failed to unban user",
426 | 																		);
427 | 																		setIsLoading(undefined);
428 | 																	},
429 | 																	onSuccess() {
430 | 																		queryClient.invalidateQueries({
431 | 																			queryKey: ["users"],
432 | 																		});
433 | 																		toast.success("User unbanned successfully");
434 | 																	},
435 | 																},
436 | 															);
437 | 															queryClient.invalidateQueries({
438 | 																queryKey: ["users"],
439 | 															});
440 | 														} else {
441 | 															setIsBanDialogOpen(true);
442 | 														}
443 | 													}}
444 | 													disabled={isLoading?.startsWith("ban")}
445 | 												>
446 | 													{isLoading === `ban-${user.id}` ? (
447 | 														<Loader2 className="h-4 w-4 animate-spin" />
448 | 													) : user.banned ? (
449 | 														"Unban"
450 | 													) : (
451 | 														"Ban"
452 | 													)}
453 | 												</Button>
454 | 											</div>
455 | 										</TableCell>
456 | 									</TableRow>
457 | 								))}
458 | 							</TableBody>
459 | 						</Table>
460 | 					)}
461 | 				</CardContent>
462 | 			</Card>
463 | 		</div>
464 | 	);
465 | }
466 | 
```

--------------------------------------------------------------------------------
/docs/scripts/endpoint-to-doc/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { createAuthEndpoint as BAcreateAuthEndpoint } from "better-auth/api";
  2 | import fs from "fs";
  3 | import path from "path";
  4 | import { z } from "zod";
  5 | 
  6 | playSound("Hero");
  7 | 
  8 | let isUsingSessionMiddleware = false;
  9 | 
 10 | export const {
 11 | 	orgMiddleware,
 12 | 	orgSessionMiddleware,
 13 | 	requestOnlySessionMiddleware,
 14 | 	sessionMiddleware,
 15 | 	originCheck,
 16 | 	adminMiddleware,
 17 | 	referenceMiddleware,
 18 | } = {
 19 | 	orgMiddleware: () => {},
 20 | 	referenceMiddleware: (cb: (x: any) => void) => () => {},
 21 | 	orgSessionMiddleware: () => {},
 22 | 	requestOnlySessionMiddleware: () => {},
 23 | 	sessionMiddleware: () => {
 24 | 		isUsingSessionMiddleware = true;
 25 | 	},
 26 | 	originCheck: (cb: (x: any) => void) => () => {},
 27 | 	adminMiddleware: () => {
 28 | 		isUsingSessionMiddleware = true;
 29 | 	},
 30 | };
 31 | 
 32 | const file = path.join(process.cwd(), "./scripts/endpoint-to-doc/input.ts");
 33 | 
 34 | function clearImportCache() {
 35 | 	const resolved = new URL(file, import.meta.url).pathname;
 36 | 	delete (globalThis as any).__dynamicImportCache?.[resolved];
 37 | 	delete require.cache[require.resolve(resolved)];
 38 | }
 39 | 
 40 | console.log(`Watching: ${file}`);
 41 | 
 42 | fs.watch(file, async () => {
 43 | 	isUsingSessionMiddleware = false;
 44 | 	playSound();
 45 | 	console.log(`Detected file change. Regenerating mdx.`);
 46 | 	const inputCode = fs.readFileSync(file, "utf-8");
 47 | 	if (inputCode.includes(".coerce"))
 48 | 		fs.writeFileSync(file, inputCode.replaceAll(".coerce", ""), "utf-8");
 49 | 	await generateMDX();
 50 | 	playSound("Hero");
 51 | });
 52 | 
 53 | async function generateMDX() {
 54 | 	const exports = await import("./input");
 55 | 	clearImportCache();
 56 | 	if (Object.keys(exports).length !== 1)
 57 | 		return console.error(`Please provide at least 1 export.`);
 58 | 	const start = Date.now();
 59 | 	const functionName = Object.keys(exports)[0]! as string;
 60 | 
 61 | 	const [path, options]: [string, Options] =
 62 | 		//@ts-expect-error
 63 | 		await exports[Object.keys(exports)[0]!];
 64 | 	if (!path || !options) return console.error(`No path or options.`);
 65 | 
 66 | 	if (options.use) {
 67 | 		options.use.forEach((fn) => fn());
 68 | 	}
 69 | 
 70 | 	console.log(`function name:`, functionName);
 71 | 
 72 | 	let jsdoc = generateJSDoc({
 73 | 		path,
 74 | 		functionName,
 75 | 		options,
 76 | 		isServerOnly: options.metadata?.SERVER_ONLY ?? false,
 77 | 	});
 78 | 
 79 | 	let mdx = `<APIMethod${parseParams(path, options)}>\n\`\`\`ts\n${parseType(
 80 | 		functionName,
 81 | 		options,
 82 | 	)}\n\`\`\`\n</APIMethod>`;
 83 | 
 84 | 	console.log(`Generated in ${(Date.now() - start).toFixed(2)}ms!`);
 85 | 	fs.writeFileSync(
 86 | 		"./scripts/endpoint-to-doc/output.mdx",
 87 | 		`${APIMethodsHeader}\n\n${mdx}\n\n${JSDocHeader}\n\n${jsdoc}`,
 88 | 		"utf-8",
 89 | 	);
 90 | 	console.log(`Successfully updated \`output.mdx\`!`);
 91 | }
 92 | 
 93 | type CreateAuthEndpointProps = Parameters<typeof BAcreateAuthEndpoint>;
 94 | 
 95 | type Options = CreateAuthEndpointProps[1];
 96 | 
 97 | const APIMethodsHeader = `{/* -------------------------------------------------------- */}
 98 | {/*                   APIMethod component                    */}
 99 | {/* -------------------------------------------------------- */}`;
100 | 
101 | const JSDocHeader = `{/* -------------------------------------------------------- */}
102 | {/*                JSDOC For the endpoint                    */}
103 | {/* -------------------------------------------------------- */}`;
104 | 
105 | export const createAuthEndpoint = async (
106 | 	...params: Partial<CreateAuthEndpointProps>
107 | ) => {
108 | 	const [path, options] = params;
109 | 	if (!path || !options) return console.error(`No path or options.`);
110 | 
111 | 	return [path, options];
112 | };
113 | 
114 | type Body = {
115 | 	propName: string;
116 | 	type: string[];
117 | 	isOptional: boolean;
118 | 	isServerOnly: boolean;
119 | 	jsDocComment: string | null;
120 | 	path: string[];
121 | 	example: string | undefined;
122 | };
123 | 
124 | function parseType(functionName: string, options: Options) {
125 | 	const body: z.ZodAny = (options.query ?? options.body) as any;
126 | 
127 | 	const parsedBody: Body[] = parseZodShape(body, []);
128 | 
129 | 	// console.log(parsedBody);
130 | 
131 | 	let strBody: string = convertBodyToString(parsedBody);
132 | 
133 | 	return `type ${functionName} = {\n${strBody}}`;
134 | }
135 | 
136 | function convertBodyToString(parsedBody: Body[]) {
137 | 	let strBody: string = ``;
138 | 	const indentationSpaces = `    `;
139 | 
140 | 	let i = -1;
141 | 	for (const body of parsedBody) {
142 | 		i++;
143 | 		if (body.jsDocComment || body.isServerOnly) {
144 | 			strBody += `${indentationSpaces.repeat(
145 | 				1 + body.path.length,
146 | 			)}/**\n${indentationSpaces.repeat(1 + body.path.length)} * ${
147 | 				body.jsDocComment
148 | 			} ${
149 | 				body.isServerOnly
150 | 					? `\n${indentationSpaces.repeat(1 + body.path.length)} * @serverOnly`
151 | 					: ""
152 | 			}\n${indentationSpaces.repeat(1 + body.path.length)} */\n`;
153 | 		}
154 | 
155 | 		if (body.type[0] === "Object") {
156 | 			strBody += `${indentationSpaces.repeat(1 + body.path.length)}${
157 | 				body.propName
158 | 			}${body.isOptional ? "?" : ""}: {\n`;
159 | 		} else {
160 | 			strBody += `${indentationSpaces.repeat(1 + body.path.length)}${
161 | 				body.propName
162 | 			}${body.isOptional ? "?" : ""}: ${body.type.join(" | ")}${
163 | 				typeof body.example !== "undefined" ? ` = ${body.example}` : ""
164 | 			}\n`;
165 | 		}
166 | 
167 | 		if (
168 | 			!parsedBody[i + 1] ||
169 | 			parsedBody[i + 1].path.length < body.path.length
170 | 		) {
171 | 			let diff = body.path.length - (parsedBody[i + 1]?.path?.length || 0);
172 | 			for (const index of Array(diff)
173 | 				.fill(0)
174 | 				.map((_, i) => i)
175 | 				.reverse()) {
176 | 				strBody += `${indentationSpaces.repeat(index + 1)}}\n`;
177 | 			}
178 | 		}
179 | 	}
180 | 
181 | 	return strBody;
182 | }
183 | 
184 | function parseZodShape(zod: z.ZodAny, path: string[]) {
185 | 	const parsedBody: Body[] = [];
186 | 
187 | 	if (!zod || !zod._def) {
188 | 		return parsedBody;
189 | 	}
190 | 
191 | 	let isRootOptional = undefined;
192 | 	let shape = z.object(
193 | 		{ test: z.string({ description: "" }) },
194 | 		{ description: "some descriptiom" },
195 | 	).shape;
196 | 
197 | 	//@ts-expect-error
198 | 	if (zod._def.typeName === "ZodOptional") {
199 | 		isRootOptional = true;
200 | 		const eg = z.optional(z.object({}));
201 | 		const x = zod as never as typeof eg;
202 | 		//@ts-expect-error
203 | 		shape = x._def.innerType.shape;
204 | 	} else {
205 | 		const eg = z.object({});
206 | 		const x = zod as never as typeof eg;
207 | 		//@ts-expect-error
208 | 		shape = x.shape;
209 | 	}
210 | 
211 | 	for (const [key, value] of Object.entries(shape)) {
212 | 		if (!value) continue;
213 | 		let description = value.description;
214 | 		let { type, isOptional, defaultValue } = getType(value as any, {
215 | 			forceOptional: isRootOptional,
216 | 		});
217 | 
218 | 		let example = description ? description.split(" Eg: ")[1] : undefined;
219 | 		if (example) description = description?.replace(" Eg: " + example, "");
220 | 
221 | 		let isServerOnly = description
222 | 			? description.includes("server-only.")
223 | 			: false;
224 | 		if (isServerOnly) description = description?.replace(" server-only. ", "");
225 | 
226 | 		if (!description?.trim().length) description = undefined;
227 | 
228 | 		parsedBody.push({
229 | 			propName: key,
230 | 			isOptional: isOptional,
231 | 			jsDocComment: description ?? null,
232 | 			path,
233 | 			isServerOnly,
234 | 			type,
235 | 			example: example ?? defaultValue ?? undefined,
236 | 		});
237 | 
238 | 		if (type[0] === "Object") {
239 | 			const v = value as never as z.ZodAny;
240 | 			parsedBody.push(...parseZodShape(v, [...path, key]));
241 | 		}
242 | 	}
243 | 	return parsedBody;
244 | }
245 | 
246 | function getType(
247 | 	value: z.ZodAny,
248 | 	{
249 | 		forceNullable,
250 | 		forceOptional,
251 | 		forceDefaultValue,
252 | 	}: {
253 | 		forceOptional?: boolean;
254 | 		forceNullable?: boolean;
255 | 		forceDefaultValue?: string;
256 | 	} = {},
257 | ): { type: string[]; isOptional: boolean; defaultValue?: string } {
258 | 	if (!value._def) {
259 | 		console.error(
260 | 			`Something went wrong during "getType". value._def isn't defined.`,
261 | 		);
262 | 		console.error(`value:`);
263 | 		console.log(value);
264 | 		process.exit(1);
265 | 	}
266 | 	const _null: string[] = value?.isNullable() ? ["null"] : [];
267 | 	switch (value._def.typeName as string) {
268 | 		case "ZodString": {
269 | 			return {
270 | 				type: ["string", ..._null],
271 | 				isOptional: forceOptional ?? value.isOptional(),
272 | 				defaultValue: forceDefaultValue,
273 | 			};
274 | 		}
275 | 		case "ZodObject": {
276 | 			return {
277 | 				type: ["Object", ..._null],
278 | 				isOptional: forceOptional ?? value.isOptional(),
279 | 				defaultValue: forceDefaultValue,
280 | 			};
281 | 		}
282 | 		case "ZodBoolean": {
283 | 			return {
284 | 				type: ["boolean", ..._null],
285 | 				isOptional: forceOptional ?? value.isOptional(),
286 | 				defaultValue: forceDefaultValue,
287 | 			};
288 | 		}
289 | 		case "ZodDate": {
290 | 			return {
291 | 				type: ["date", ..._null],
292 | 				isOptional: forceOptional ?? value.isOptional(),
293 | 				defaultValue: forceDefaultValue,
294 | 			};
295 | 		}
296 | 		case "ZodEnum": {
297 | 			const v = value as never as z.ZodEnum<["hello", "world"]>;
298 | 			const types: string[] = [];
299 | 			for (const value of v._def.values) {
300 | 				types.push(JSON.stringify(value));
301 | 			}
302 | 			return {
303 | 				type: types,
304 | 				isOptional: forceOptional ?? v.isOptional(),
305 | 				defaultValue: forceDefaultValue,
306 | 			};
307 | 		}
308 | 		case "ZodOptional": {
309 | 			const v = value as never as z.ZodOptional<z.ZodAny>;
310 | 			const r = getType(v._def.innerType, {
311 | 				forceOptional: true,
312 | 				forceNullable: forceNullable,
313 | 			});
314 | 			return {
315 | 				type: r.type,
316 | 				isOptional: forceOptional ?? r.isOptional,
317 | 				defaultValue: forceDefaultValue,
318 | 			};
319 | 		}
320 | 		case "ZodDefault": {
321 | 			const v = value as never as z.ZodDefault<z.ZodAny>;
322 | 			const r = getType(v._def.innerType, {
323 | 				forceOptional: forceOptional,
324 | 				forceDefaultValue: JSON.stringify(v._def.defaultValue()),
325 | 				forceNullable: forceNullable,
326 | 			});
327 | 			return {
328 | 				type: r.type,
329 | 				isOptional: forceOptional ?? r.isOptional,
330 | 				defaultValue: forceDefaultValue ?? r.defaultValue,
331 | 			};
332 | 		}
333 | 		case "ZodAny": {
334 | 			return {
335 | 				type: ["any", ..._null],
336 | 				isOptional: forceOptional ?? value.isOptional(),
337 | 				defaultValue: forceDefaultValue,
338 | 			};
339 | 		}
340 | 		case "ZodRecord": {
341 | 			const v = value as never as z.ZodRecord;
342 | 			const keys: string[] = getType(v._def.keyType as any).type;
343 | 			const values: string[] = getType(v._def.valueType as any).type;
344 | 			return {
345 | 				type: keys.map((key, i) => `Record<${key}, ${values[i]}>`),
346 | 				isOptional: forceOptional ?? v.isOptional(),
347 | 				defaultValue: forceDefaultValue,
348 | 			};
349 | 		}
350 | 		case "ZodNumber": {
351 | 			return {
352 | 				type: ["number", ..._null],
353 | 				isOptional: forceOptional ?? value.isOptional(),
354 | 				defaultValue: forceDefaultValue,
355 | 			};
356 | 		}
357 | 		case "ZodUnion": {
358 | 			const v = value as never as z.ZodUnion<[z.ZodAny]>;
359 | 			const types: string[] = [];
360 | 			for (const option of v.options) {
361 | 				const t = getType(option as any).type;
362 | 				types.push(t.length === 0 ? t[0] : `${t.join(" | ")}`);
363 | 			}
364 | 			return {
365 | 				type: types,
366 | 				isOptional: forceOptional ?? v.isOptional(),
367 | 				defaultValue: forceDefaultValue,
368 | 			};
369 | 		}
370 | 		case "ZodNullable": {
371 | 			const v = value as never as z.ZodNullable<z.ZodAny>;
372 | 			const r = getType(v._def.innerType, { forceOptional: true });
373 | 			return {
374 | 				type: r.type,
375 | 				isOptional: forceOptional ?? r.isOptional,
376 | 				defaultValue: forceDefaultValue,
377 | 			};
378 | 		}
379 | 
380 | 		case "ZodArray": {
381 | 			const v = value as never as z.ZodArray<z.ZodAny>;
382 | 			const types = getType(v._def.type as any);
383 | 			return {
384 | 				type: [
385 | 					`${
386 | 						types.type.length === 1
387 | 							? types.type[0]
388 | 							: `(${types.type.join(" | ")})`
389 | 					}[]`,
390 | 					..._null,
391 | 				],
392 | 				isOptional: forceOptional ?? v.isOptional(),
393 | 				defaultValue: forceDefaultValue,
394 | 			};
395 | 		}
396 | 
397 | 		default: {
398 | 			console.error(`Unknown Zod type: ${value._def.typeName}`);
399 | 			console.log(value._def);
400 | 			process.exit(1);
401 | 		}
402 | 	}
403 | }
404 | 
405 | function parseParams(path: string, options: Options): string {
406 | 	let params: string[] = [];
407 | 	params.push(`path="${path}"`);
408 | 	params.push(`method="${options.method}"`);
409 | 
410 | 	if (options.requireHeaders || isUsingSessionMiddleware)
411 | 		params.push("requireSession");
412 | 	if (options.metadata?.SERVER_ONLY) params.push("isServerOnly");
413 | 	if (options.method === "GET" && options.body) params.push("forceAsBody");
414 | 	if (options.method === "POST" && options.query) params.push("forceAsQuery");
415 | 
416 | 	if (params.length === 2) return " " + params.join(" ");
417 | 	return "\n  " + params.join("\n  ") + "\n";
418 | }
419 | 
420 | function generateJSDoc({
421 | 	path,
422 | 	options,
423 | 	functionName,
424 | 	isServerOnly,
425 | }: {
426 | 	path: string;
427 | 	options: Options;
428 | 	functionName: string;
429 | 	isServerOnly: boolean;
430 | }) {
431 | 	/**
432 | 	 * ### Endpoint
433 | 	 *
434 | 	 * POST `/organization/set-active`
435 | 	 *
436 | 	 * ### API Methods
437 | 	 *
438 | 	 * **server:**
439 | 	 * `auth.api.setActiveOrganization`
440 | 	 *
441 | 	 * **client:**
442 | 	 * `authClient.organization.setActive`
443 | 	 *
444 | 	 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-set-active)
445 | 	 */
446 | 
447 | 	let jsdoc: string[] = [];
448 | 	jsdoc.push(`### Endpoint`);
449 | 	jsdoc.push(``);
450 | 	jsdoc.push(`${options.method} \`${path}\``);
451 | 	jsdoc.push(``);
452 | 	jsdoc.push(`### API Methods`);
453 | 	jsdoc.push(``);
454 | 	jsdoc.push(`**server:**`);
455 | 	jsdoc.push(`\`auth.api.${functionName}\``);
456 | 	jsdoc.push(``);
457 | 	if (!isServerOnly) {
458 | 		jsdoc.push(`**client:**`);
459 | 		jsdoc.push(`\`authClient.${pathToDotNotation(path)}\``);
460 | 		jsdoc.push(``);
461 | 	}
462 | 	jsdoc.push(
463 | 		`@see [Read our docs to learn more.](https://better-auth.com/docs/plugins/${
464 | 			path.split("/")[1]
465 | 		}#api-method${path.replaceAll("/", "-")})`,
466 | 	);
467 | 
468 | 	return `/**\n * ${jsdoc.join("\n * ")}\n */`;
469 | }
470 | 
471 | function pathToDotNotation(input: string): string {
472 | 	return input
473 | 		.split("/") // split into segments
474 | 		.filter(Boolean) // remove empty strings (from leading '/')
475 | 		.map((segment) =>
476 | 			segment
477 | 				.split("-") // split kebab-case
478 | 				.map((word, i) =>
479 | 					i === 0
480 | 						? word.toLowerCase()
481 | 						: word.charAt(0).toUpperCase() + word.slice(1),
482 | 				)
483 | 				.join(""),
484 | 		)
485 | 		.join(".");
486 | }
487 | 
488 | function playSound(name: string = "Ping") {
489 | 	const path = `/System/Library/Sounds/${name}.aiff`;
490 | 	void Bun.$`afplay ${path}`;
491 | }
492 | 
```

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

```typescript
  1 | "use client";
  2 | 
  3 | import { betterFetch } from "@better-fetch/fetch";
  4 | import { atom } from "jotai";
  5 | import { AlertCircle, Bot, Send, User } from "lucide-react";
  6 | import { useEffect, useRef, useState } from "react";
  7 | import {
  8 | 	Dialog,
  9 | 	DialogContent,
 10 | 	DialogDescription,
 11 | 	DialogHeader,
 12 | 	DialogTitle,
 13 | } from "@/components/ui/dialog";
 14 | import { Textarea } from "@/components/ui/textarea";
 15 | import { cn } from "@/lib/utils";
 16 | import { MarkdownRenderer } from "./markdown-renderer";
 17 | 
 18 | interface Message {
 19 | 	id: string;
 20 | 	role: "user" | "assistant";
 21 | 	content: string;
 22 | 	timestamp: Date;
 23 | 	isStreaming?: boolean;
 24 | }
 25 | 
 26 | export const aiChatModalAtom = atom(false);
 27 | 
 28 | interface AIChatModalProps {
 29 | 	isOpen: boolean;
 30 | 	onClose: () => void;
 31 | }
 32 | 
 33 | export function AIChatModal({ isOpen, onClose }: AIChatModalProps) {
 34 | 	const [messages, setMessages] = useState<Message[]>([]);
 35 | 	const [input, setInput] = useState("");
 36 | 	const [isLoading, setIsLoading] = useState(false);
 37 | 	const [apiError, setApiError] = useState<string | null>(null);
 38 | 	const [sessionId, setSessionId] = useState<string | null>(null);
 39 | 	const [externalUserId] = useState<string>(
 40 | 		() =>
 41 | 			`better-auth-user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
 42 | 	);
 43 | 	const messagesEndRef = useRef<HTMLDivElement>(null);
 44 | 	const abortControllerRef = useRef<AbortController | null>(null);
 45 | 
 46 | 	const scrollToBottom = () => {
 47 | 		messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
 48 | 	};
 49 | 
 50 | 	useEffect(() => {
 51 | 		scrollToBottom();
 52 | 	}, [messages]);
 53 | 
 54 | 	useEffect(() => {
 55 | 		return () => {
 56 | 			if (abortControllerRef.current) {
 57 | 				abortControllerRef.current.abort();
 58 | 			}
 59 | 		};
 60 | 	}, []);
 61 | 
 62 | 	useEffect(() => {
 63 | 		if (!isOpen) {
 64 | 			setSessionId(null);
 65 | 			setMessages([]);
 66 | 			setInput("");
 67 | 			setApiError(null);
 68 | 		}
 69 | 	}, [isOpen]);
 70 | 
 71 | 	const handleSubmit = async (e: React.FormEvent) => {
 72 | 		e.preventDefault();
 73 | 		if (!input.trim() || isLoading) return;
 74 | 
 75 | 		const userMessage: Message = {
 76 | 			id: Date.now().toString(),
 77 | 			role: "user",
 78 | 			content: input.trim(),
 79 | 			timestamp: new Date(),
 80 | 		};
 81 | 
 82 | 		setMessages((prev) => [...prev, userMessage]);
 83 | 		setInput("");
 84 | 		setIsLoading(true);
 85 | 		setApiError(null);
 86 | 
 87 | 		const thinkingMessage: Message = {
 88 | 			id: `thinking-${Date.now()}`,
 89 | 			role: "assistant",
 90 | 			content: "",
 91 | 			timestamp: new Date(),
 92 | 			isStreaming: false,
 93 | 		};
 94 | 
 95 | 		setMessages((prev) => [...prev, thinkingMessage]);
 96 | 
 97 | 		abortControllerRef.current = new AbortController();
 98 | 
 99 | 		try {
100 | 			const payload = {
101 | 				question: userMessage.content,
102 | 				stream: false, // Use non-streaming to get session_id
103 | 				session_id: sessionId, // Use existing session_id if available
104 | 				external_user_id: externalUserId, // Use consistent external_user_id for consistency on getting the context right
105 | 				fetch_existing: false,
106 | 			};
107 | 
108 | 			const { data, error } = await betterFetch<{
109 | 				content?: string;
110 | 				answer?: string;
111 | 				response?: string;
112 | 				session_id?: string;
113 | 			}>("/api/ai-chat", {
114 | 				method: "POST",
115 | 				headers: {
116 | 					"content-type": "application/json",
117 | 				},
118 | 				body: JSON.stringify(payload),
119 | 				signal: abortControllerRef.current.signal,
120 | 			});
121 | 
122 | 			if (error) {
123 | 				console.error("API Error Response:", error);
124 | 				throw new Error(`HTTP ${error.status}: ${error.message}`);
125 | 			}
126 | 
127 | 			if (data.session_id) {
128 | 				setSessionId(data.session_id);
129 | 			}
130 | 
131 | 			let answer = "";
132 | 			if (data.content) {
133 | 				answer = data.content;
134 | 			} else if (data.answer) {
135 | 				answer = data.answer;
136 | 			} else if (data.response) {
137 | 				answer = data.response;
138 | 			} else if (typeof data === "string") {
139 | 				answer = data;
140 | 			} else {
141 | 				console.error("Unexpected response format:", data);
142 | 				throw new Error("Unexpected response format from API");
143 | 			}
144 | 
145 | 			await simulateStreamingEffect(answer, thinkingMessage.id);
146 | 		} catch (error) {
147 | 			if (error instanceof Error && error.name === "AbortError") {
148 | 				console.log("Request was aborted");
149 | 				return;
150 | 			}
151 | 
152 | 			console.error("Error calling AI API:", error);
153 | 
154 | 			setMessages((prev) =>
155 | 				prev.map((msg) =>
156 | 					msg.id.startsWith("thinking-")
157 | 						? {
158 | 								id: (Date.now() + 1).toString(),
159 | 								role: "assistant" as const,
160 | 								content: `I encountered an error while processing your request. Please try again.`,
161 | 								timestamp: new Date(),
162 | 								isStreaming: false,
163 | 							}
164 | 						: msg,
165 | 				),
166 | 			);
167 | 
168 | 			if (error instanceof Error) {
169 | 				setApiError(error.message);
170 | 			}
171 | 		} finally {
172 | 			setIsLoading(false);
173 | 			abortControllerRef.current = null;
174 | 		}
175 | 	};
176 | 
177 | 	const simulateStreamingEffect = async (
178 | 		fullContent: string,
179 | 		thinkingMessageId: string,
180 | 	) => {
181 | 		const assistantMessageId = (Date.now() + 1).toString();
182 | 		let displayedContent = "";
183 | 
184 | 		setMessages((prev) =>
185 | 			prev.map((msg) =>
186 | 				msg.id === thinkingMessageId
187 | 					? {
188 | 							id: assistantMessageId,
189 | 							role: "assistant" as const,
190 | 							content: "",
191 | 							timestamp: new Date(),
192 | 							isStreaming: true,
193 | 						}
194 | 					: msg,
195 | 			),
196 | 		);
197 | 
198 | 		const words = fullContent.split(" ");
199 | 		for (let i = 0; i < words.length; i++) {
200 | 			displayedContent += (i > 0 ? " " : "") + words[i];
201 | 
202 | 			setMessages((prev) =>
203 | 				prev.map((msg) =>
204 | 					msg.id === assistantMessageId
205 | 						? { ...msg, content: displayedContent }
206 | 						: msg,
207 | 				),
208 | 			);
209 | 
210 | 			const delay = Math.random() * 50 + 20;
211 | 			await new Promise((resolve) => setTimeout(resolve, delay));
212 | 		}
213 | 
214 | 		setMessages((prev) =>
215 | 			prev.map((msg) =>
216 | 				msg.id === assistantMessageId ? { ...msg, isStreaming: false } : msg,
217 | 			),
218 | 		);
219 | 	};
220 | 
221 | 	return (
222 | 		<Dialog open={isOpen} onOpenChange={onClose}>
223 | 			<DialogContent className="max-w-4xl border-b h-[80vh] flex flex-col">
224 | 				<DialogHeader>
225 | 					<DialogTitle className="flex items-center gap-2">
226 | 						<Bot className="h-5 w-5 text-primary" />
227 | 						Ask AI About Better Auth
228 | 					</DialogTitle>
229 | 					<DialogDescription>
230 | 						Ask questions about Better-Auth and get AI-powered answers
231 | 						{apiError && (
232 | 							<div className="flex items-center gap-2 mt-2 text-amber-600 dark:text-amber-400">
233 | 								<AlertCircle className="h-4 w-4" />
234 | 								<span className="text-xs">
235 | 									API Error: Something went wrong. Please try again.
236 | 								</span>
237 | 							</div>
238 | 						)}
239 | 					</DialogDescription>
240 | 				</DialogHeader>
241 | 
242 | 				<div className="flex-1 flex flex-col min-h-0">
243 | 					<div
244 | 						className={cn(
245 | 							"flex-1 overflow-y-auto space-y-4 p-6",
246 | 							messages.length === 0 ? "overflow-y-hidden" : "overflow-y-auto",
247 | 						)}
248 | 					>
249 | 						{messages.length === 0 ? (
250 | 							<div className="flex h-full flex-col items-center justify-center text-center">
251 | 								<div className="mb-6">
252 | 									<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">
253 | 										<Bot className="h-8 w-8 text-primary" />
254 | 									</div>
255 | 								</div>
256 | 
257 | 								<div className="mb-8 max-w-md">
258 | 									<h3 className="text-xl font-semibold text-foreground mb-2">
259 | 										Ask About Better Auth
260 | 									</h3>
261 | 									<p className="text-muted-foreground text-sm leading-relaxed">
262 | 										I'm here to help you with Better Auth questions, setup
263 | 										guides, and implementation tips. Ask me anything!
264 | 									</p>
265 | 								</div>
266 | 
267 | 								<div className="w-full max-w-lg">
268 | 									<p className="text-sm font-medium text-foreground mb-4">
269 | 										Try asking:
270 | 									</p>
271 | 									<div className="space-y-3">
272 | 										{[
273 | 											"How do I set up SSO with Google?",
274 | 											"How to integrate Better Auth with NextJs?",
275 | 											"How to setup Two Factor Authentication?",
276 | 										].map((question, index) => (
277 | 											<button
278 | 												key={index}
279 | 												onClick={() => setInput(question)}
280 | 												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"
281 | 											>
282 | 												<div className="flex items-center gap-3">
283 | 													<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">
284 | 														<span className="text-xs text-primary font-medium">
285 | 															{index + 1}
286 | 														</span>
287 | 													</div>
288 | 													<span className="text-sm text-foreground group-hover:text-primary transition-colors">
289 | 														{question}
290 | 													</span>
291 | 												</div>
292 | 											</button>
293 | 										))}
294 | 									</div>
295 | 								</div>
296 | 							</div>
297 | 						) : (
298 | 							messages.map((message) => (
299 | 								<div
300 | 									key={message.id}
301 | 									className={cn(
302 | 										"flex gap-3",
303 | 										message.role === "user" ? "justify-end" : "justify-start",
304 | 									)}
305 | 								>
306 | 									{message.role === "assistant" && (
307 | 										<div className="flex-shrink-0">
308 | 											<div className="w-8 h-8 rounded-full bg-transparent border border-input/70 border-dashed flex items-center justify-center">
309 | 												<Bot className="h-4 w-4 text-primary" />
310 | 											</div>
311 | 										</div>
312 | 									)}
313 | 									<div
314 | 										className={cn(
315 | 											"max-w-[80%] rounded-xl px-4 py-3 shadow-sm",
316 | 											message.role === "user"
317 | 												? "bg-primary text-primary-foreground"
318 | 												: "bg-background border border-border/50",
319 | 										)}
320 | 									>
321 | 										{message.role === "assistant" ? (
322 | 											<div className="w-full">
323 | 												{message.id.startsWith("thinking-") ? (
324 | 													<div className="flex items-center gap-2 text-sm text-muted-foreground">
325 | 														<div className="flex space-x-1">
326 | 															<div className="w-1 h-1 bg-primary rounded-full animate-bounce [animation-delay:-0.3s]"></div>
327 | 															<div className="w-1 h-1 bg-primary rounded-full animate-bounce [animation-delay:-0.15s]"></div>
328 | 															<div className="w-1 h-1 bg-primary rounded-full animate-bounce"></div>
329 | 														</div>
330 | 														<span>Thinking...</span>
331 | 													</div>
332 | 												) : (
333 | 													<>
334 | 														<MarkdownRenderer content={message.content} />
335 | 														{message.isStreaming && (
336 | 															<div className="inline-block w-2 h-4 bg-primary streaming-cursor ml-1" />
337 | 														)}
338 | 													</>
339 | 												)}
340 | 											</div>
341 | 										) : (
342 | 											<p className="text-sm">{message.content}</p>
343 | 										)}
344 | 									</div>
345 | 									{message.role === "user" && (
346 | 										<div className="flex-shrink-0">
347 | 											<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
348 | 												<User className="h-4 w-4" />
349 | 											</div>
350 | 										</div>
351 | 									)}
352 | 								</div>
353 | 							))
354 | 						)}
355 | 						<div ref={messagesEndRef} />
356 | 					</div>
357 | 
358 | 					<div className="border-t px-0 bg-background/50 backdrop-blur-sm p-4">
359 | 						<div className="relative max-w-4xl mx-auto">
360 | 							<div
361 | 								className={cn(
362 | 									"relative flex flex-col border-input rounded-lg transition-all duration-200 w-full text-left",
363 | 									"ring-1 ring-border/20 bg-muted/30 border-input border-1 backdrop-blur-sm",
364 | 									"focus-within:ring-primary/30 focus-within:bg-muted/[35%]",
365 | 								)}
366 | 							>
367 | 								<div className="overflow-y-auto max-h-[200px]">
368 | 									<Textarea
369 | 										value={input}
370 | 										onChange={(e) => setInput(e.target.value)}
371 | 										placeholder="Ask a question about Better-Auth..."
372 | 										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"
373 | 										disabled={isLoading}
374 | 										onKeyDown={(e) => {
375 | 											if (e.key === "Enter" && !e.shiftKey) {
376 | 												e.preventDefault();
377 | 												void handleSubmit(e);
378 | 											}
379 | 										}}
380 | 									/>
381 | 								</div>
382 | 
383 | 								<div className="h-12 bg-muted/20 rounded-b-xl flex items-center justify-end px-3">
384 | 									<button
385 | 										type="submit"
386 | 										onClick={(e) => {
387 | 											e.preventDefault();
388 | 											void handleSubmit(e);
389 | 										}}
390 | 										disabled={!input.trim() || isLoading}
391 | 										className={cn(
392 | 											"rounded-lg p-2 transition-all duration-200",
393 | 											input.trim() && !isLoading
394 | 												? "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md"
395 | 												: "bg-muted/50 text-muted-foreground cursor-not-allowed",
396 | 										)}
397 | 									>
398 | 										{isLoading ? (
399 | 											<div className="w-4 h-4 border-2 border-current/30 border-t-current rounded-full animate-spin" />
400 | 										) : (
401 | 											<Send className="h-4 w-4" />
402 | 										)}
403 | 									</button>
404 | 								</div>
405 | 							</div>
406 | 						</div>
407 | 
408 | 						<div className="mt-3 text-center">
409 | 							<p className="text-xs text-muted-foreground">
410 | 								Press{" "}
411 | 								<kbd className="px-1.5 py-0.5 text-xs bg-muted rounded">
412 | 									Enter
413 | 								</kbd>{" "}
414 | 								to send,{" "}
415 | 								<kbd className="px-1.5 py-0.5 text-xs bg-muted rounded">
416 | 									Shift+Enter
417 | 								</kbd>{" "}
418 | 								for new line
419 | 							</p>
420 | 						</div>
421 | 					</div>
422 | 				</div>
423 | 			</DialogContent>
424 | 		</Dialog>
425 | 	);
426 | }
427 | 
```

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

```typescript
  1 | import type { AuthContext } from "@better-auth/core";
  2 | import { createAuthEndpoint } from "@better-auth/core/api";
  3 | import * as z from "zod";
  4 | import { APIError, getSessionFromCtx } from "../../../api";
  5 | import { getDate } from "../../../utils/date";
  6 | import { safeJSONParse } from "../../../utils/json";
  7 | import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
  8 | import { defaultKeyHasher } from "../";
  9 | import { apiKeySchema } from "../schema";
 10 | import type { ApiKey } from "../types";
 11 | import type { PredefinedApiKeyOptions } from ".";
 12 | 
 13 | export function createApiKey({
 14 | 	keyGenerator,
 15 | 	opts,
 16 | 	schema,
 17 | 	deleteAllExpiredApiKeys,
 18 | }: {
 19 | 	keyGenerator: (options: {
 20 | 		length: number;
 21 | 		prefix: string | undefined;
 22 | 	}) => Promise<string> | string;
 23 | 	opts: PredefinedApiKeyOptions;
 24 | 	schema: ReturnType<typeof apiKeySchema>;
 25 | 	deleteAllExpiredApiKeys(
 26 | 		ctx: AuthContext,
 27 | 		byPassLastCheckTime?: boolean,
 28 | 	): void;
 29 | }) {
 30 | 	return createAuthEndpoint(
 31 | 		"/api-key/create",
 32 | 		{
 33 | 			method: "POST",
 34 | 			body: z.object({
 35 | 				name: z
 36 | 					.string()
 37 | 					.meta({ description: "Name of the Api Key" })
 38 | 					.optional(),
 39 | 				expiresIn: z
 40 | 					.number()
 41 | 					.meta({
 42 | 						description: "Expiration time of the Api Key in seconds",
 43 | 					})
 44 | 					.min(1)
 45 | 					.optional()
 46 | 					.nullable()
 47 | 					.default(null),
 48 | 
 49 | 				userId: z.coerce
 50 | 					.string()
 51 | 					.meta({
 52 | 						description:
 53 | 							'User Id of the user that the Api Key belongs to. server-only. Eg: "user-id"',
 54 | 					})
 55 | 					.optional(),
 56 | 				prefix: z
 57 | 					.string()
 58 | 					.meta({ description: "Prefix of the Api Key" })
 59 | 					.regex(/^[a-zA-Z0-9_-]+$/, {
 60 | 						message:
 61 | 							"Invalid prefix format, must be alphanumeric and contain only underscores and hyphens.",
 62 | 					})
 63 | 					.optional(),
 64 | 				remaining: z
 65 | 					.number()
 66 | 					.meta({
 67 | 						description: "Remaining number of requests. Server side only",
 68 | 					})
 69 | 					.min(0)
 70 | 					.optional()
 71 | 					.nullable()
 72 | 					.default(null),
 73 | 				metadata: z.any().optional(),
 74 | 				refillAmount: z
 75 | 					.number()
 76 | 					.meta({
 77 | 						description:
 78 | 							"Amount to refill the remaining count of the Api Key. server-only. Eg: 100",
 79 | 					})
 80 | 					.min(1)
 81 | 					.optional(),
 82 | 				refillInterval: z
 83 | 					.number()
 84 | 					.meta({
 85 | 						description:
 86 | 							"Interval to refill the Api Key in milliseconds. server-only. Eg: 1000",
 87 | 					})
 88 | 					.optional(),
 89 | 				rateLimitTimeWindow: z
 90 | 					.number()
 91 | 					.meta({
 92 | 						description:
 93 | 							"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",
 94 | 					})
 95 | 					.optional(),
 96 | 				rateLimitMax: z
 97 | 					.number()
 98 | 					.meta({
 99 | 						description:
100 | 							"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",
101 | 					})
102 | 					.optional(),
103 | 				rateLimitEnabled: z
104 | 					.boolean()
105 | 					.meta({
106 | 						description:
107 | 							"Whether the key has rate limiting enabled. server-only. Eg: true",
108 | 					})
109 | 					.optional(),
110 | 				permissions: z
111 | 					.record(z.string(), z.array(z.string()))
112 | 					.meta({
113 | 						description: "Permissions of the Api Key.",
114 | 					})
115 | 					.optional(),
116 | 			}),
117 | 			metadata: {
118 | 				openapi: {
119 | 					description: "Create a new API key for a user",
120 | 					responses: {
121 | 						"200": {
122 | 							description: "API key created successfully",
123 | 							content: {
124 | 								"application/json": {
125 | 									schema: {
126 | 										type: "object",
127 | 										properties: {
128 | 											id: {
129 | 												type: "string",
130 | 												description: "Unique identifier of the API key",
131 | 											},
132 | 											createdAt: {
133 | 												type: "string",
134 | 												format: "date-time",
135 | 												description: "Creation timestamp",
136 | 											},
137 | 											updatedAt: {
138 | 												type: "string",
139 | 												format: "date-time",
140 | 												description: "Last update timestamp",
141 | 											},
142 | 											name: {
143 | 												type: "string",
144 | 												nullable: true,
145 | 												description: "Name of the API key",
146 | 											},
147 | 											prefix: {
148 | 												type: "string",
149 | 												nullable: true,
150 | 												description: "Prefix of the API key",
151 | 											},
152 | 											start: {
153 | 												type: "string",
154 | 												nullable: true,
155 | 												description:
156 | 													"Starting characters of the key (if configured)",
157 | 											},
158 | 											key: {
159 | 												type: "string",
160 | 												description:
161 | 													"The full API key (only returned on creation)",
162 | 											},
163 | 											enabled: {
164 | 												type: "boolean",
165 | 												description: "Whether the key is enabled",
166 | 											},
167 | 											expiresAt: {
168 | 												type: "string",
169 | 												format: "date-time",
170 | 												nullable: true,
171 | 												description: "Expiration timestamp",
172 | 											},
173 | 											userId: {
174 | 												type: "string",
175 | 												description: "ID of the user owning the key",
176 | 											},
177 | 											lastRefillAt: {
178 | 												type: "string",
179 | 												format: "date-time",
180 | 												nullable: true,
181 | 												description: "Last refill timestamp",
182 | 											},
183 | 											lastRequest: {
184 | 												type: "string",
185 | 												format: "date-time",
186 | 												nullable: true,
187 | 												description: "Last request timestamp",
188 | 											},
189 | 											metadata: {
190 | 												type: "object",
191 | 												nullable: true,
192 | 												additionalProperties: true,
193 | 												description: "Metadata associated with the key",
194 | 											},
195 | 											rateLimitMax: {
196 | 												type: "number",
197 | 												nullable: true,
198 | 												description: "Maximum requests in time window",
199 | 											},
200 | 											rateLimitTimeWindow: {
201 | 												type: "number",
202 | 												nullable: true,
203 | 												description: "Rate limit time window in milliseconds",
204 | 											},
205 | 											remaining: {
206 | 												type: "number",
207 | 												nullable: true,
208 | 												description: "Remaining requests",
209 | 											},
210 | 											refillAmount: {
211 | 												type: "number",
212 | 												nullable: true,
213 | 												description: "Amount to refill",
214 | 											},
215 | 											refillInterval: {
216 | 												type: "number",
217 | 												nullable: true,
218 | 												description: "Refill interval in milliseconds",
219 | 											},
220 | 											rateLimitEnabled: {
221 | 												type: "boolean",
222 | 												description: "Whether rate limiting is enabled",
223 | 											},
224 | 											requestCount: {
225 | 												type: "number",
226 | 												description: "Current request count in window",
227 | 											},
228 | 											permissions: {
229 | 												type: "object",
230 | 												nullable: true,
231 | 												additionalProperties: {
232 | 													type: "array",
233 | 													items: { type: "string" },
234 | 												},
235 | 												description: "Permissions associated with the key",
236 | 											},
237 | 										},
238 | 										required: [
239 | 											"id",
240 | 											"createdAt",
241 | 											"updatedAt",
242 | 											"key",
243 | 											"enabled",
244 | 											"userId",
245 | 											"rateLimitEnabled",
246 | 											"requestCount",
247 | 										],
248 | 									},
249 | 								},
250 | 							},
251 | 						},
252 | 					},
253 | 				},
254 | 			},
255 | 		},
256 | 		async (ctx) => {
257 | 			const {
258 | 				name,
259 | 				expiresIn,
260 | 				prefix,
261 | 				remaining,
262 | 				metadata,
263 | 				refillAmount,
264 | 				refillInterval,
265 | 				permissions,
266 | 				rateLimitMax,
267 | 				rateLimitTimeWindow,
268 | 				rateLimitEnabled,
269 | 			} = ctx.body;
270 | 
271 | 			const session = await getSessionFromCtx(ctx);
272 | 			const authRequired = ctx.request || ctx.headers;
273 | 			const user =
274 | 				authRequired && !session
275 | 					? null
276 | 					: session?.user || { id: ctx.body.userId };
277 | 
278 | 			if (!user?.id) {
279 | 				throw new APIError("UNAUTHORIZED", {
280 | 					message: ERROR_CODES.UNAUTHORIZED_SESSION,
281 | 				});
282 | 			}
283 | 
284 | 			if (session && ctx.body.userId && session?.user.id !== ctx.body.userId) {
285 | 				throw new APIError("UNAUTHORIZED", {
286 | 					message: ERROR_CODES.UNAUTHORIZED_SESSION,
287 | 				});
288 | 			}
289 | 
290 | 			if (authRequired) {
291 | 				// if this endpoint was being called from the client,
292 | 				// we must make sure they can't use server-only properties.
293 | 				if (
294 | 					refillAmount !== undefined ||
295 | 					refillInterval !== undefined ||
296 | 					rateLimitMax !== undefined ||
297 | 					rateLimitTimeWindow !== undefined ||
298 | 					rateLimitEnabled !== undefined ||
299 | 					permissions !== undefined ||
300 | 					remaining !== null
301 | 				) {
302 | 					throw new APIError("BAD_REQUEST", {
303 | 						message: ERROR_CODES.SERVER_ONLY_PROPERTY,
304 | 					});
305 | 				}
306 | 			}
307 | 
308 | 			// if metadata is defined, than check that it's an object.
309 | 			if (metadata) {
310 | 				if (opts.enableMetadata === false) {
311 | 					throw new APIError("BAD_REQUEST", {
312 | 						message: ERROR_CODES.METADATA_DISABLED,
313 | 					});
314 | 				}
315 | 				if (typeof metadata !== "object") {
316 | 					throw new APIError("BAD_REQUEST", {
317 | 						message: ERROR_CODES.INVALID_METADATA_TYPE,
318 | 					});
319 | 				}
320 | 			}
321 | 
322 | 			// make sure that if they pass a refill amount, they also pass a refill interval
323 | 			if (refillAmount && !refillInterval) {
324 | 				throw new APIError("BAD_REQUEST", {
325 | 					message: ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
326 | 				});
327 | 			}
328 | 			// make sure that if they pass a refill interval, they also pass a refill amount
329 | 			if (refillInterval && !refillAmount) {
330 | 				throw new APIError("BAD_REQUEST", {
331 | 					message: ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
332 | 				});
333 | 			}
334 | 
335 | 			if (expiresIn) {
336 | 				if (opts.keyExpiration.disableCustomExpiresTime === true) {
337 | 					throw new APIError("BAD_REQUEST", {
338 | 						message: ERROR_CODES.KEY_DISABLED_EXPIRATION,
339 | 					});
340 | 				}
341 | 
342 | 				const expiresIn_in_days = expiresIn / (60 * 60 * 24);
343 | 
344 | 				if (opts.keyExpiration.minExpiresIn > expiresIn_in_days) {
345 | 					throw new APIError("BAD_REQUEST", {
346 | 						message: ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
347 | 					});
348 | 				} else if (opts.keyExpiration.maxExpiresIn < expiresIn_in_days) {
349 | 					throw new APIError("BAD_REQUEST", {
350 | 						message: ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
351 | 					});
352 | 				}
353 | 			}
354 | 			if (prefix) {
355 | 				if (prefix.length < opts.minimumPrefixLength) {
356 | 					throw new APIError("BAD_REQUEST", {
357 | 						message: ERROR_CODES.INVALID_PREFIX_LENGTH,
358 | 					});
359 | 				}
360 | 				if (prefix.length > opts.maximumPrefixLength) {
361 | 					throw new APIError("BAD_REQUEST", {
362 | 						message: ERROR_CODES.INVALID_PREFIX_LENGTH,
363 | 					});
364 | 				}
365 | 			}
366 | 
367 | 			if (name) {
368 | 				if (name.length < opts.minimumNameLength) {
369 | 					throw new APIError("BAD_REQUEST", {
370 | 						message: ERROR_CODES.INVALID_NAME_LENGTH,
371 | 					});
372 | 				}
373 | 				if (name.length > opts.maximumNameLength) {
374 | 					throw new APIError("BAD_REQUEST", {
375 | 						message: ERROR_CODES.INVALID_NAME_LENGTH,
376 | 					});
377 | 				}
378 | 			} else if (opts.requireName) {
379 | 				throw new APIError("BAD_REQUEST", {
380 | 					message: ERROR_CODES.NAME_REQUIRED,
381 | 				});
382 | 			}
383 | 
384 | 			deleteAllExpiredApiKeys(ctx.context);
385 | 
386 | 			const key = await keyGenerator({
387 | 				length: opts.defaultKeyLength,
388 | 				prefix: prefix || opts.defaultPrefix,
389 | 			});
390 | 
391 | 			const hashed = opts.disableKeyHashing ? key : await defaultKeyHasher(key);
392 | 
393 | 			let start: string | null = null;
394 | 
395 | 			if (opts.startingCharactersConfig.shouldStore) {
396 | 				start = key.substring(
397 | 					0,
398 | 					opts.startingCharactersConfig.charactersLength,
399 | 				);
400 | 			}
401 | 
402 | 			const defaultPermissions = opts.permissions?.defaultPermissions
403 | 				? typeof opts.permissions.defaultPermissions === "function"
404 | 					? await opts.permissions.defaultPermissions(user.id, ctx)
405 | 					: opts.permissions.defaultPermissions
406 | 				: undefined;
407 | 			const permissionsToApply = permissions
408 | 				? JSON.stringify(permissions)
409 | 				: defaultPermissions
410 | 					? JSON.stringify(defaultPermissions)
411 | 					: undefined;
412 | 
413 | 			let data: Omit<ApiKey, "id"> = {
414 | 				createdAt: new Date(),
415 | 				updatedAt: new Date(),
416 | 				name: name ?? null,
417 | 				prefix: prefix ?? opts.defaultPrefix ?? null,
418 | 				start: start,
419 | 				key: hashed,
420 | 				enabled: true,
421 | 				expiresAt: expiresIn
422 | 					? getDate(expiresIn, "sec")
423 | 					: opts.keyExpiration.defaultExpiresIn
424 | 						? getDate(opts.keyExpiration.defaultExpiresIn, "sec")
425 | 						: null,
426 | 				userId: user.id,
427 | 				lastRefillAt: null,
428 | 				lastRequest: null,
429 | 				metadata: null,
430 | 				rateLimitMax: rateLimitMax ?? opts.rateLimit.maxRequests ?? null,
431 | 				rateLimitTimeWindow:
432 | 					rateLimitTimeWindow ?? opts.rateLimit.timeWindow ?? null,
433 | 				remaining:
434 | 					remaining === null ? remaining : (remaining ?? refillAmount ?? null),
435 | 				refillAmount: refillAmount ?? null,
436 | 				refillInterval: refillInterval ?? null,
437 | 				rateLimitEnabled:
438 | 					rateLimitEnabled === undefined
439 | 						? (opts.rateLimit.enabled ?? true)
440 | 						: rateLimitEnabled,
441 | 				requestCount: 0,
442 | 				//@ts-expect-error - we intentionally save the permissions as string on DB.
443 | 				permissions: permissionsToApply,
444 | 			};
445 | 
446 | 			if (metadata) {
447 | 				//@ts-expect-error - we intentionally save the metadata as string on DB.
448 | 				data.metadata = schema.apikey.fields.metadata.transform.input(metadata);
449 | 			}
450 | 
451 | 			const apiKey = await ctx.context.adapter.create<
452 | 				Omit<ApiKey, "id">,
453 | 				ApiKey
454 | 			>({
455 | 				model: API_KEY_TABLE_NAME,
456 | 				data: data,
457 | 			});
458 | 
459 | 			return ctx.json({
460 | 				...(apiKey as ApiKey),
461 | 				key: key,
462 | 				metadata: metadata ?? null,
463 | 				permissions: apiKey.permissions
464 | 					? safeJSONParse(apiKey.permissions)
465 | 					: null,
466 | 			});
467 | 		},
468 | 	);
469 | }
470 | 
```

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

```typescript
  1 | import {
  2 | 	createAuthEndpoint,
  3 | 	createAuthMiddleware,
  4 | } from "@better-auth/core/api";
  5 | import { APIError } from "better-call";
  6 | import { describe, expect, it } from "vitest";
  7 | import * as z from "zod";
  8 | import { init } from "../init";
  9 | import { getTestInstance } from "../test-utils/test-instance";
 10 | import { toAuthEndpoints } from "./to-auth-endpoints";
 11 | 
 12 | describe("before hook", async () => {
 13 | 	describe("context", async () => {
 14 | 		const endpoints = {
 15 | 			query: createAuthEndpoint(
 16 | 				"/query",
 17 | 				{
 18 | 					method: "GET",
 19 | 				},
 20 | 				async (c) => {
 21 | 					return c.query;
 22 | 				},
 23 | 			),
 24 | 			body: createAuthEndpoint(
 25 | 				"/body",
 26 | 				{
 27 | 					method: "POST",
 28 | 				},
 29 | 				async (c) => {
 30 | 					return c.body;
 31 | 				},
 32 | 			),
 33 | 			params: createAuthEndpoint(
 34 | 				"/params",
 35 | 				{
 36 | 					method: "GET",
 37 | 				},
 38 | 				async (c) => {
 39 | 					return c.params;
 40 | 				},
 41 | 			),
 42 | 			headers: createAuthEndpoint(
 43 | 				"/headers",
 44 | 				{
 45 | 					method: "GET",
 46 | 					requireHeaders: true,
 47 | 				},
 48 | 				async (c) => {
 49 | 					return Object.fromEntries(c.headers.entries());
 50 | 				},
 51 | 			),
 52 | 		};
 53 | 
 54 | 		const authContext = init({
 55 | 			hooks: {
 56 | 				before: createAuthMiddleware(async (c) => {
 57 | 					switch (c.path) {
 58 | 						case "/body":
 59 | 							return {
 60 | 								context: {
 61 | 									body: {
 62 | 										name: "body",
 63 | 									},
 64 | 								},
 65 | 							};
 66 | 						case "/params":
 67 | 							return {
 68 | 								context: {
 69 | 									params: {
 70 | 										name: "params",
 71 | 									},
 72 | 								},
 73 | 							};
 74 | 						case "/headers":
 75 | 							return {
 76 | 								context: {
 77 | 									headers: new Headers({
 78 | 										name: "headers",
 79 | 									}),
 80 | 								},
 81 | 							};
 82 | 					}
 83 | 					return {
 84 | 						context: {
 85 | 							query: {
 86 | 								name: "query",
 87 | 							},
 88 | 						},
 89 | 					};
 90 | 				}),
 91 | 			},
 92 | 		});
 93 | 		const authEndpoints = toAuthEndpoints(endpoints, authContext);
 94 | 
 95 | 		it("should return hook set query", async () => {
 96 | 			const res = await authEndpoints.query();
 97 | 			expect(res?.name).toBe("query");
 98 | 			const res2 = await authEndpoints.query({
 99 | 				query: {
100 | 					key: "value",
101 | 				},
102 | 			});
103 | 			expect(res2).toMatchObject({
104 | 				name: "query",
105 | 				key: "value",
106 | 			});
107 | 		});
108 | 
109 | 		it("should return hook set body", async () => {
110 | 			const res = await authEndpoints.body();
111 | 			expect(res?.name).toBe("body");
112 | 			const res2 = await authEndpoints.body({
113 | 				//@ts-expect-error
114 | 				body: {
115 | 					key: "value",
116 | 				},
117 | 			});
118 | 			expect(res2).toMatchObject({
119 | 				name: "body",
120 | 				key: "value",
121 | 			});
122 | 		});
123 | 
124 | 		it("should return hook set param", async () => {
125 | 			const res = await authEndpoints.params();
126 | 			expect(res?.name).toBe("params");
127 | 			const res2 = await authEndpoints.params({
128 | 				params: {
129 | 					key: "value",
130 | 				},
131 | 			});
132 | 			expect(res2).toMatchObject({
133 | 				name: "params",
134 | 				key: "value",
135 | 			});
136 | 		});
137 | 
138 | 		it("should return hook set headers", async () => {
139 | 			const res = await authEndpoints.headers({
140 | 				headers: new Headers({
141 | 					key: "value",
142 | 				}),
143 | 			});
144 | 			expect(res).toMatchObject({ key: "value", name: "headers" });
145 | 		});
146 | 
147 | 		it("should replace existing array when hook provides another array", async () => {
148 | 			const endpoint = {
149 | 				body: createAuthEndpoint(
150 | 					"/body-array-replace",
151 | 					{ method: "POST", body: z.object({ tags: z.array(z.string()) }) },
152 | 					async (c) => c.body,
153 | 				),
154 | 			};
155 | 			const authContext = init({
156 | 				hooks: {
157 | 					before: createAuthMiddleware(async (c) => {
158 | 						if (c.path === "/body-array-replace") {
159 | 							return {
160 | 								context: {
161 | 									body: {
162 | 										tags: ["a"],
163 | 									},
164 | 								},
165 | 							};
166 | 						}
167 | 					}),
168 | 				},
169 | 			});
170 | 			const api = toAuthEndpoints(endpoint, authContext);
171 | 
172 | 			const res = await api.body({
173 | 				body: {
174 | 					tags: ["b", "c"],
175 | 				},
176 | 			});
177 | 			expect(res.tags).toEqual(["a"]);
178 | 		});
179 | 	});
180 | 
181 | 	describe("response", async () => {
182 | 		const endpoints = {
183 | 			response: createAuthEndpoint(
184 | 				"/response",
185 | 				{
186 | 					method: "GET",
187 | 				},
188 | 				async (c) => {
189 | 					return { response: true };
190 | 				},
191 | 			),
192 | 			json: createAuthEndpoint(
193 | 				"/json",
194 | 				{
195 | 					method: "GET",
196 | 				},
197 | 				async (c) => {
198 | 					return { response: true };
199 | 				},
200 | 			),
201 | 		};
202 | 
203 | 		const authContext = init({
204 | 			hooks: {
205 | 				before: createAuthMiddleware(async (c) => {
206 | 					if (c.path === "/json") {
207 | 						return { before: true };
208 | 					}
209 | 					return new Response(JSON.stringify({ before: true }));
210 | 				}),
211 | 			},
212 | 		});
213 | 		const authEndpoints = toAuthEndpoints(endpoints, authContext);
214 | 
215 | 		it("should return Response object", async () => {
216 | 			const response = await authEndpoints.response();
217 | 			expect(response).toBeInstanceOf(Response);
218 | 		});
219 | 
220 | 		it("should return the hook response", async () => {
221 | 			const response = await authEndpoints.json();
222 | 			expect(response).toMatchObject({ before: true });
223 | 		});
224 | 	});
225 | });
226 | 
227 | describe("after hook", async () => {
228 | 	describe("response", async () => {
229 | 		const endpoints = {
230 | 			changeResponse: createAuthEndpoint(
231 | 				"/change-response",
232 | 				{
233 | 					method: "GET",
234 | 				},
235 | 				async (c) => {
236 | 					return {
237 | 						hello: "world",
238 | 					};
239 | 				},
240 | 			),
241 | 			throwError: createAuthEndpoint(
242 | 				"/throw-error",
243 | 				{
244 | 					method: "POST",
245 | 					query: z
246 | 						.object({
247 | 							throwHook: z.boolean(),
248 | 						})
249 | 						.optional(),
250 | 				},
251 | 				async (c) => {
252 | 					throw c.error("BAD_REQUEST");
253 | 				},
254 | 			),
255 | 			multipleHooks: createAuthEndpoint(
256 | 				"/multi-hooks",
257 | 				{
258 | 					method: "GET",
259 | 				},
260 | 				async (c) => {
261 | 					return {
262 | 						return: "1",
263 | 					};
264 | 				},
265 | 			),
266 | 		};
267 | 
268 | 		const authContext = init({
269 | 			plugins: [
270 | 				{
271 | 					id: "test",
272 | 					hooks: {
273 | 						after: [
274 | 							{
275 | 								matcher() {
276 | 									return true;
277 | 								},
278 | 								handler: createAuthMiddleware(async (c) => {
279 | 									if (c.path === "/multi-hooks") {
280 | 										return {
281 | 											return: "3",
282 | 										};
283 | 									}
284 | 								}),
285 | 							},
286 | 						],
287 | 					},
288 | 				},
289 | 			],
290 | 			hooks: {
291 | 				after: createAuthMiddleware(async (c) => {
292 | 					if (c.path === "/change-response") {
293 | 						return {
294 | 							hello: "auth",
295 | 						};
296 | 					}
297 | 					if (c.path === "/multi-hooks") {
298 | 						return {
299 | 							return: "2",
300 | 						};
301 | 					}
302 | 					if (c.query?.throwHook) {
303 | 						throw c.error("BAD_REQUEST", {
304 | 							message: "from after hook",
305 | 						});
306 | 					}
307 | 				}),
308 | 			},
309 | 		});
310 | 
311 | 		const api = toAuthEndpoints(endpoints, authContext);
312 | 
313 | 		it("should change the response object from `hello:world` to `hello:auth`", async () => {
314 | 			const response = await api.changeResponse();
315 | 			expect(response).toMatchObject({ hello: "auth" });
316 | 		});
317 | 
318 | 		it("should return the last hook returned response", async () => {
319 | 			const response = await api.multipleHooks();
320 | 			expect(response).toMatchObject({
321 | 				return: "3",
322 | 			});
323 | 		});
324 | 
325 | 		it("should return error as response", async () => {
326 | 			const response = await api.throwError({
327 | 				asResponse: true,
328 | 			});
329 | 			expect(response.status).toBe(400);
330 | 		});
331 | 
332 | 		it("should throw the last error", async () => {
333 | 			await api
334 | 				.throwError({
335 | 					query: {
336 | 						throwHook: true,
337 | 					},
338 | 				})
339 | 				.catch((e) => {
340 | 					expect(e).toBeInstanceOf(APIError);
341 | 					expect(e?.message).toBe("from after hook");
342 | 				});
343 | 		});
344 | 	});
345 | 
346 | 	describe("cookies", async () => {
347 | 		const endpoints = {
348 | 			cookies: createAuthEndpoint(
349 | 				"/cookies",
350 | 				{
351 | 					method: "POST",
352 | 				},
353 | 				async (c) => {
354 | 					c.setCookie("session", "value");
355 | 					return { hello: "world" };
356 | 				},
357 | 			),
358 | 			cookieOverride: createAuthEndpoint(
359 | 				"/cookie",
360 | 				{
361 | 					method: "GET",
362 | 				},
363 | 				async (c) => {
364 | 					c.setCookie("data", "1");
365 | 				},
366 | 			),
367 | 			noCookie: createAuthEndpoint(
368 | 				"/no-cookie",
369 | 				{
370 | 					method: "GET",
371 | 				},
372 | 				async (c) => {},
373 | 			),
374 | 		};
375 | 
376 | 		const authContext = init({
377 | 			hooks: {
378 | 				after: createAuthMiddleware(async (c) => {
379 | 					c.setHeader("key", "value");
380 | 					c.setCookie("data", "2");
381 | 				}),
382 | 			},
383 | 		});
384 | 
385 | 		const authEndpoints = toAuthEndpoints(endpoints, authContext);
386 | 
387 | 		it("set cookies from both hook", async () => {
388 | 			const result = await authEndpoints.cookies({
389 | 				asResponse: true,
390 | 			});
391 | 			expect(result.headers.get("set-cookie")).toContain("session=value");
392 | 			expect(result.headers.get("set-cookie")).toContain("data=2");
393 | 		});
394 | 
395 | 		it("should override cookie", async () => {
396 | 			const result = await authEndpoints.cookieOverride({
397 | 				asResponse: true,
398 | 			});
399 | 			expect(result.headers.get("set-cookie")).toContain("data=2");
400 | 		});
401 | 
402 | 		it("should only set the hook cookie", async () => {
403 | 			const result = await authEndpoints.noCookie({
404 | 				asResponse: true,
405 | 			});
406 | 			expect(result.headers.get("set-cookie")).toContain("data=2");
407 | 		});
408 | 
409 | 		it("should return cookies from return headers", async () => {
410 | 			const result = await authEndpoints.noCookie({
411 | 				returnHeaders: true,
412 | 			});
413 | 			expect(result.headers.get("set-cookie")).toContain("data=2");
414 | 
415 | 			const result2 = await authEndpoints.cookies({
416 | 				asResponse: true,
417 | 			});
418 | 			expect(result2.headers.get("set-cookie")).toContain("session=value");
419 | 			expect(result2.headers.get("set-cookie")).toContain("data=2");
420 | 		});
421 | 	});
422 | });
423 | 
424 | describe("disabled paths", async () => {
425 | 	const { client } = await getTestInstance({
426 | 		disabledPaths: ["/sign-in/email"],
427 | 	});
428 | 
429 | 	it("should return 404 for disabled paths", async () => {
430 | 		const response = await client.$fetch("/ok");
431 | 		expect(response.data).toEqual({ ok: true });
432 | 		const { error } = await client.signIn.email({
433 | 			email: "[email protected]",
434 | 			password: "test",
435 | 		});
436 | 		expect(error?.status).toBe(404);
437 | 	});
438 | });
439 | 
440 | describe("debug mode stack trace", () => {
441 | 	it("should preserve stack trace when logger is in debug mode and APIError is thrown", async () => {
442 | 		const endpoints = {
443 | 			testEndpoint: createAuthEndpoint(
444 | 				"/test-error",
445 | 				{ method: "GET" },
446 | 				async () => {
447 | 					throw new APIError("BAD_REQUEST", { message: "Test error" });
448 | 				},
449 | 			),
450 | 		};
451 | 
452 | 		const authContext = init({
453 | 			logger: {
454 | 				level: "debug",
455 | 			},
456 | 		});
457 | 
458 | 		const api = toAuthEndpoints(endpoints, authContext);
459 | 
460 | 		try {
461 | 			await api.testEndpoint({});
462 | 		} catch (error: any) {
463 | 			expect(error).toBeInstanceOf(APIError);
464 | 			expect(error.stack).toBeDefined();
465 | 			expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
466 | 			expect(error.stack).toMatch(/at\s+/);
467 | 		}
468 | 	});
469 | 
470 | 	it("should not modify stack trace when logger is not in debug mode", async () => {
471 | 		const endpoints = {
472 | 			testEndpoint: createAuthEndpoint(
473 | 				"/test-error",
474 | 				{ method: "GET" },
475 | 				async () => {
476 | 					throw new APIError("BAD_REQUEST", { message: "Test error" });
477 | 				},
478 | 			),
479 | 		};
480 | 
481 | 		const authContext = init({
482 | 			logger: {
483 | 				level: "error", // Not debug mode
484 | 			},
485 | 		});
486 | 
487 | 		const api = toAuthEndpoints(endpoints, authContext);
488 | 
489 | 		try {
490 | 			await api.testEndpoint({});
491 | 		} catch (error: any) {
492 | 			expect(error).toBeInstanceOf(APIError);
493 | 			// Stack should exist but may be minimal when not in debug mode
494 | 			expect(error.stack).toBeDefined();
495 | 		}
496 | 	});
497 | 
498 | 	it("should have detailed stack trace in debug mode", async () => {
499 | 		const endpoints = {
500 | 			testEndpoint: createAuthEndpoint(
501 | 				"/test-error",
502 | 				{ method: "GET" },
503 | 				async () => {
504 | 					throw new APIError("INTERNAL_SERVER_ERROR", {
505 | 						message: "Internal error occurred",
506 | 					});
507 | 				},
508 | 			),
509 | 		};
510 | 
511 | 		const authContext = init({
512 | 			logger: {
513 | 				level: "debug",
514 | 			},
515 | 		});
516 | 
517 | 		const api = toAuthEndpoints(endpoints, authContext);
518 | 
519 | 		try {
520 | 			await api.testEndpoint({});
521 | 		} catch (error: any) {
522 | 			expect(error).toBeInstanceOf(APIError);
523 | 			expect(error.stack).toBeDefined();
524 | 			// Check for stack trace format
525 | 			expect(error.stack).toMatch(/at\s+.*\(.*\)/); // Match "at functionName (file:line:col)"
526 | 			expect(error.stack).toMatch(/\.ts:\d+:\d+/); // Match TypeScript file with line:column
527 | 		}
528 | 	});
529 | 
530 | 	it("should handle APIError in hooks with debug mode", async () => {
531 | 		const endpoints = {
532 | 			testEndpoint: createAuthEndpoint(
533 | 				"/test-hook-error",
534 | 				{ method: "GET" },
535 | 				async () => {
536 | 					return { data: "success" };
537 | 				},
538 | 			),
539 | 		};
540 | 
541 | 		const authContext = init({
542 | 			logger: {
543 | 				level: "debug",
544 | 			},
545 | 			hooks: {
546 | 				before: createAuthMiddleware(async () => {
547 | 					throw new APIError("FORBIDDEN", { message: "Forbidden action" });
548 | 				}),
549 | 			},
550 | 		});
551 | 
552 | 		const api = toAuthEndpoints(endpoints, authContext);
553 | 
554 | 		try {
555 | 			await api.testEndpoint({});
556 | 		} catch (error: any) {
557 | 			expect(error).toBeInstanceOf(APIError);
558 | 			expect(error.stack).toBeDefined();
559 | 			expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
560 | 			expect(error.stack).toMatch(/at\s+/);
561 | 		}
562 | 	});
563 | 
564 | 	it("should handle Response containing APIError in debug mode", async () => {
565 | 		const endpoints = {
566 | 			testEndpoint: createAuthEndpoint(
567 | 				"/test-response-error",
568 | 				{ method: "GET" },
569 | 				async () => {
570 | 					throw new APIError("UNAUTHORIZED", {
571 | 						message: "Unauthorized access",
572 | 					});
573 | 				},
574 | 			),
575 | 		};
576 | 
577 | 		const authContext = init({
578 | 			logger: {
579 | 				level: "debug",
580 | 			},
581 | 		});
582 | 
583 | 		const api = toAuthEndpoints(endpoints, authContext);
584 | 
585 | 		// Test with asResponse = true to get Response object
586 | 		const response = await api.testEndpoint({ asResponse: true });
587 | 		expect(response).toBeInstanceOf(Response);
588 | 		expect(response.status).toBe(401);
589 | 
590 | 		// Test with asResponse = false to get thrown error
591 | 		try {
592 | 			await api.testEndpoint({ asResponse: false });
593 | 		} catch (error: any) {
594 | 			expect(error).toBeInstanceOf(APIError);
595 | 			expect(error.stack).toBeDefined();
596 | 			expect(error.stack).toMatch(/ErrorWithStack:|Error:|APIError:/);
597 | 		}
598 | 	});
599 | });
600 | 
```

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

```typescript
  1 | import type { BetterAuthClientPlugin, ClientStore } from "@better-auth/core";
  2 | import type { BetterFetchOption } from "@better-fetch/fetch";
  3 | import Constants from "expo-constants";
  4 | import * as Linking from "expo-linking";
  5 | import { Platform } from "react-native";
  6 | 
  7 | interface CookieAttributes {
  8 | 	value: string;
  9 | 	expires?: Date;
 10 | 	"max-age"?: number;
 11 | 	domain?: string;
 12 | 	path?: string;
 13 | 	secure?: boolean;
 14 | 	httpOnly?: boolean;
 15 | 	sameSite?: "Strict" | "Lax" | "None";
 16 | }
 17 | 
 18 | export function parseSetCookieHeader(
 19 | 	header: string,
 20 | ): Map<string, CookieAttributes> {
 21 | 	const cookieMap = new Map<string, CookieAttributes>();
 22 | 	const cookies = splitSetCookieHeader(header);
 23 | 	cookies.forEach((cookie) => {
 24 | 		const parts = cookie.split(";").map((p) => p.trim());
 25 | 		const [nameValue, ...attributes] = parts;
 26 | 		const [name, ...valueParts] = nameValue!.split("=");
 27 | 		const value = valueParts.join("=");
 28 | 		const cookieObj: CookieAttributes = { value };
 29 | 		attributes.forEach((attr) => {
 30 | 			const [attrName, ...attrValueParts] = attr.split("=");
 31 | 			const attrValue = attrValueParts.join("=");
 32 | 			cookieObj[attrName!.toLowerCase() as "value"] = attrValue;
 33 | 		});
 34 | 		cookieMap.set(name!, cookieObj);
 35 | 	});
 36 | 	return cookieMap;
 37 | }
 38 | 
 39 | function splitSetCookieHeader(setCookie: string): string[] {
 40 | 	const parts: string[] = [];
 41 | 	let buffer = "";
 42 | 	let i = 0;
 43 | 	while (i < setCookie.length) {
 44 | 		const char = setCookie[i];
 45 | 		if (char === ",") {
 46 | 			const recent = buffer.toLowerCase();
 47 | 			const hasExpires = recent.includes("expires=");
 48 | 			const hasGmt = /gmt/i.test(recent);
 49 | 			if (hasExpires && !hasGmt) {
 50 | 				buffer += char;
 51 | 				i += 1;
 52 | 				continue;
 53 | 			}
 54 | 			if (buffer.trim().length > 0) {
 55 | 				parts.push(buffer.trim());
 56 | 				buffer = "";
 57 | 			}
 58 | 			i += 1;
 59 | 			if (setCookie[i] === " ") i += 1;
 60 | 			continue;
 61 | 		}
 62 | 		buffer += char;
 63 | 		i += 1;
 64 | 	}
 65 | 	if (buffer.trim().length > 0) {
 66 | 		parts.push(buffer.trim());
 67 | 	}
 68 | 	return parts;
 69 | }
 70 | 
 71 | interface ExpoClientOptions {
 72 | 	scheme?: string;
 73 | 	storage: {
 74 | 		setItem: (key: string, value: string) => any;
 75 | 		getItem: (key: string) => string | null;
 76 | 	};
 77 | 	/**
 78 | 	 * Prefix for local storage keys (e.g., "my-app_cookie", "my-app_session_data")
 79 | 	 * @default "better-auth"
 80 | 	 */
 81 | 	storagePrefix?: string;
 82 | 	/**
 83 | 	 * Prefix for server cookie names to filter (e.g., "better-auth.session_token")
 84 | 	 * This is used to identify which cookies belong to better-auth to prevent
 85 | 	 * infinite refetching when third-party cookies are set.
 86 | 	 * @default "better-auth"
 87 | 	 */
 88 | 	cookiePrefix?: string;
 89 | 	disableCache?: boolean;
 90 | }
 91 | 
 92 | interface StoredCookie {
 93 | 	value: string;
 94 | 	expires: string | null;
 95 | }
 96 | 
 97 | export function getSetCookie(header: string, prevCookie?: string) {
 98 | 	const parsed = parseSetCookieHeader(header);
 99 | 	let toSetCookie: Record<string, StoredCookie> = {};
100 | 	parsed.forEach((cookie, key) => {
101 | 		const expiresAt = cookie["expires"];
102 | 		const maxAge = cookie["max-age"];
103 | 		const expires = maxAge
104 | 			? new Date(Date.now() + Number(maxAge) * 1000)
105 | 			: expiresAt
106 | 				? new Date(String(expiresAt))
107 | 				: null;
108 | 		toSetCookie[key] = {
109 | 			value: cookie["value"],
110 | 			expires: expires ? expires.toISOString() : null,
111 | 		};
112 | 	});
113 | 	if (prevCookie) {
114 | 		try {
115 | 			const prevCookieParsed = JSON.parse(prevCookie);
116 | 			toSetCookie = {
117 | 				...prevCookieParsed,
118 | 				...toSetCookie,
119 | 			};
120 | 		} catch {
121 | 			//
122 | 		}
123 | 	}
124 | 	return JSON.stringify(toSetCookie);
125 | }
126 | 
127 | export function getCookie(cookie: string) {
128 | 	let parsed = {} as Record<string, StoredCookie>;
129 | 	try {
130 | 		parsed = JSON.parse(cookie) as Record<string, StoredCookie>;
131 | 	} catch (e) {}
132 | 	const toSend = Object.entries(parsed).reduce((acc, [key, value]) => {
133 | 		if (value.expires && new Date(value.expires) < new Date()) {
134 | 			return acc;
135 | 		}
136 | 		return `${acc}; ${key}=${value.value}`;
137 | 	}, "");
138 | 	return toSend;
139 | }
140 | 
141 | function getOrigin(scheme: string) {
142 | 	const schemeURI = Linking.createURL("", { scheme });
143 | 	return schemeURI;
144 | }
145 | 
146 | /**
147 |  * Compare if session cookies have actually changed by comparing their values.
148 |  * Ignores expiry timestamps that naturally change on each request.
149 |  *
150 |  * @param prevCookie - Previous cookie JSON string
151 |  * @param newCookie - New cookie JSON string
152 |  * @returns true if session cookies have changed, false otherwise
153 |  */
154 | function hasSessionCookieChanged(
155 | 	prevCookie: string | null,
156 | 	newCookie: string,
157 | ): boolean {
158 | 	if (!prevCookie) return true;
159 | 
160 | 	try {
161 | 		const prev = JSON.parse(prevCookie) as Record<string, StoredCookie>;
162 | 		const next = JSON.parse(newCookie) as Record<string, StoredCookie>;
163 | 
164 | 		// Get all session-related cookie keys (session_token, session_data)
165 | 		const sessionKeys = new Set<string>();
166 | 		Object.keys(prev).forEach((key) => {
167 | 			if (key.includes("session_token") || key.includes("session_data")) {
168 | 				sessionKeys.add(key);
169 | 			}
170 | 		});
171 | 		Object.keys(next).forEach((key) => {
172 | 			if (key.includes("session_token") || key.includes("session_data")) {
173 | 				sessionKeys.add(key);
174 | 			}
175 | 		});
176 | 
177 | 		// Compare the values of session cookies (ignore expires timestamps)
178 | 		for (const key of sessionKeys) {
179 | 			const prevValue = prev[key]?.value;
180 | 			const nextValue = next[key]?.value;
181 | 			if (prevValue !== nextValue) {
182 | 				return true;
183 | 			}
184 | 		}
185 | 
186 | 		return false;
187 | 	} catch {
188 | 		// If parsing fails, assume cookie changed
189 | 		return true;
190 | 	}
191 | }
192 | 
193 | /**
194 |  * Check if the Set-Cookie header contains session-related better-auth cookies.
195 |  * Only triggers session updates when session_token or session_data cookies are present.
196 |  * This prevents infinite refetching when non-session cookies (like third-party cookies) change.
197 |  *
198 |  * Supports multiple cookie naming patterns:
199 |  * - Default: "better-auth.session_token", "__Secure-better-auth.session_token"
200 |  * - Custom prefix: "myapp.session_token", "__Secure-myapp.session_token"
201 |  * - Custom full names: "my_custom_session_token", "custom_session_data"
202 |  * - No prefix (cookiePrefix=""): "session_token", "my_session_token", etc.
203 |  *
204 |  * @param setCookieHeader - The Set-Cookie header value
205 |  * @param cookiePrefix - The cookie prefix to check for. Can be empty string for custom cookie names.
206 |  * @returns true if the header contains session-related cookies, false otherwise
207 |  */
208 | export function hasBetterAuthCookies(
209 | 	setCookieHeader: string,
210 | 	cookiePrefix: string,
211 | ): boolean {
212 | 	const cookies = parseSetCookieHeader(setCookieHeader);
213 | 	const sessionCookieSuffixes = ["session_token", "session_data"];
214 | 
215 | 	// Check if any cookie is a session-related cookie
216 | 	for (const name of cookies.keys()) {
217 | 		// Remove __Secure- prefix if present for comparison
218 | 		const nameWithoutSecure = name.startsWith("__Secure-")
219 | 			? name.slice(9)
220 | 			: name;
221 | 
222 | 		for (const suffix of sessionCookieSuffixes) {
223 | 			if (cookiePrefix) {
224 | 				// When prefix is provided, only match exact pattern: "prefix.suffix"
225 | 				if (nameWithoutSecure === `${cookiePrefix}.${suffix}`) {
226 | 					return true;
227 | 				}
228 | 			} else {
229 | 				// When prefix is empty, check for:
230 | 				// 1. Exact match: "session_token"
231 | 				// 2. Custom names ending with suffix: "my_custom_session_token"
232 | 				if (nameWithoutSecure.endsWith(suffix)) {
233 | 					return true;
234 | 				}
235 | 			}
236 | 		}
237 | 	}
238 | 	return false;
239 | }
240 | 
241 | /**
242 |  * Expo secure store does not support colons in the keys.
243 |  * This function replaces colons with underscores.
244 |  *
245 |  * @see https://github.com/better-auth/better-auth/issues/5426
246 |  *
247 |  * @param name cookie name to be saved in the storage
248 |  * @returns normalized cookie name
249 |  */
250 | export function normalizeCookieName(name: string) {
251 | 	return name.replace(/:/g, "_");
252 | }
253 | 
254 | export function storageAdapter(storage: {
255 | 	getItem: (name: string) => string | null;
256 | 	setItem: (name: string, value: string) => void;
257 | }) {
258 | 	return {
259 | 		getItem: (name: string) => {
260 | 			return storage.getItem(normalizeCookieName(name));
261 | 		},
262 | 		setItem: (name: string, value: string) => {
263 | 			return storage.setItem(normalizeCookieName(name), value);
264 | 		},
265 | 	};
266 | }
267 | 
268 | export const expoClient = (opts: ExpoClientOptions) => {
269 | 	let store: ClientStore | null = null;
270 | 	const storagePrefix = opts?.storagePrefix || "better-auth";
271 | 	const cookieName = `${storagePrefix}_cookie`;
272 | 	const localCacheName = `${storagePrefix}_session_data`;
273 | 	const storage = storageAdapter(opts?.storage);
274 | 	const isWeb = Platform.OS === "web";
275 | 	const cookiePrefix = opts?.cookiePrefix || "better-auth";
276 | 
277 | 	const rawScheme =
278 | 		opts?.scheme || Constants.expoConfig?.scheme || Constants.platform?.scheme;
279 | 	const scheme = Array.isArray(rawScheme) ? rawScheme[0] : rawScheme;
280 | 
281 | 	if (!scheme && !isWeb) {
282 | 		throw new Error(
283 | 			"Scheme not found in app.json. Please provide a scheme in the options.",
284 | 		);
285 | 	}
286 | 	return {
287 | 		id: "expo",
288 | 		getActions(_, $store) {
289 | 			store = $store;
290 | 			return {
291 | 				/**
292 | 				 * Get the stored cookie.
293 | 				 *
294 | 				 * You can use this to get the cookie stored in the device and use it in your fetch
295 | 				 * requests.
296 | 				 *
297 | 				 * @example
298 | 				 * ```ts
299 | 				 * const cookie = client.getCookie();
300 | 				 * fetch("https://api.example.com", {
301 | 				 * 	headers: {
302 | 				 * 		cookie,
303 | 				 * 	},
304 | 				 * });
305 | 				 */
306 | 				getCookie: () => {
307 | 					const cookie = storage.getItem(cookieName);
308 | 					return getCookie(cookie || "{}");
309 | 				},
310 | 			};
311 | 		},
312 | 		fetchPlugins: [
313 | 			{
314 | 				id: "expo",
315 | 				name: "Expo",
316 | 				hooks: {
317 | 					async onSuccess(context) {
318 | 						if (isWeb) return;
319 | 						const setCookie = context.response.headers.get("set-cookie");
320 | 						if (setCookie) {
321 | 							// Only process and notify if the Set-Cookie header contains better-auth cookies
322 | 							// This prevents infinite refetching when other cookies (like Cloudflare's __cf_bm) are present
323 | 							if (hasBetterAuthCookies(setCookie, cookiePrefix)) {
324 | 								const prevCookie = await storage.getItem(cookieName);
325 | 								const toSetCookie = getSetCookie(
326 | 									setCookie || "",
327 | 									prevCookie ?? undefined,
328 | 								);
329 | 								// Only notify $sessionSignal if the session cookie values actually changed
330 | 								// This prevents infinite refetching when the server sends the same cookie with updated expiry
331 | 								if (hasSessionCookieChanged(prevCookie, toSetCookie)) {
332 | 									await storage.setItem(cookieName, toSetCookie);
333 | 									store?.notify("$sessionSignal");
334 | 								} else {
335 | 									// Still update the storage to refresh expiry times, but don't trigger refetch
336 | 									await storage.setItem(cookieName, toSetCookie);
337 | 								}
338 | 							}
339 | 						}
340 | 
341 | 						if (
342 | 							context.request.url.toString().includes("/get-session") &&
343 | 							!opts?.disableCache
344 | 						) {
345 | 							const data = context.data;
346 | 							storage.setItem(localCacheName, JSON.stringify(data));
347 | 						}
348 | 
349 | 						if (
350 | 							context.data?.redirect &&
351 | 							(context.request.url.toString().includes("/sign-in") ||
352 | 								context.request.url.toString().includes("/link-social")) &&
353 | 							!context.request?.body.includes("idToken") // id token is used for silent sign-in
354 | 						) {
355 | 							const callbackURL = JSON.parse(context.request.body)?.callbackURL;
356 | 							const to = callbackURL;
357 | 							const signInURL = context.data?.url;
358 | 							let Browser: typeof import("expo-web-browser") | undefined =
359 | 								undefined;
360 | 							try {
361 | 								Browser = await import("expo-web-browser");
362 | 							} catch (error) {
363 | 								throw new Error(
364 | 									'"expo-web-browser" is not installed as a dependency!',
365 | 									{
366 | 										cause: error,
367 | 									},
368 | 								);
369 | 							}
370 | 							const proxyURL = `${context.request.baseURL}/expo-authorization-proxy?authorizationURL=${encodeURIComponent(signInURL)}`;
371 | 							const result = await Browser.openAuthSessionAsync(proxyURL, to);
372 | 							if (result.type !== "success") return;
373 | 							const url = new URL(result.url);
374 | 							const cookie = String(url.searchParams.get("cookie"));
375 | 							if (!cookie) return;
376 | 							storage.setItem(cookieName, getSetCookie(cookie));
377 | 							store?.notify("$sessionSignal");
378 | 						}
379 | 					},
380 | 				},
381 | 				async init(url, options) {
382 | 					if (isWeb) {
383 | 						return {
384 | 							url,
385 | 							options: options as BetterFetchOption,
386 | 						};
387 | 					}
388 | 					options = options || {};
389 | 					const storedCookie = storage.getItem(cookieName);
390 | 					const cookie = getCookie(storedCookie || "{}");
391 | 					options.credentials = "omit";
392 | 					options.headers = {
393 | 						...options.headers,
394 | 						cookie,
395 | 						"expo-origin": getOrigin(scheme!),
396 | 						"x-skip-oauth-proxy": "true", // skip oauth proxy for expo
397 | 					};
398 | 					if (options.body?.callbackURL) {
399 | 						if (options.body.callbackURL.startsWith("/")) {
400 | 							const url = Linking.createURL(options.body.callbackURL, {
401 | 								scheme,
402 | 							});
403 | 							options.body.callbackURL = url;
404 | 						}
405 | 					}
406 | 					if (options.body?.newUserCallbackURL) {
407 | 						if (options.body.newUserCallbackURL.startsWith("/")) {
408 | 							const url = Linking.createURL(options.body.newUserCallbackURL, {
409 | 								scheme,
410 | 							});
411 | 							options.body.newUserCallbackURL = url;
412 | 						}
413 | 					}
414 | 					if (options.body?.errorCallbackURL) {
415 | 						if (options.body.errorCallbackURL.startsWith("/")) {
416 | 							const url = Linking.createURL(options.body.errorCallbackURL, {
417 | 								scheme,
418 | 							});
419 | 							options.body.errorCallbackURL = url;
420 | 						}
421 | 					}
422 | 					if (url.includes("/sign-out")) {
423 | 						await storage.setItem(cookieName, "{}");
424 | 						store?.atoms.session?.set({
425 | 							...store.atoms.session.get(),
426 | 							data: null,
427 | 							error: null,
428 | 							isPending: false,
429 | 						});
430 | 						storage.setItem(localCacheName, "{}");
431 | 					}
432 | 					return {
433 | 						url,
434 | 						options: options as BetterFetchOption,
435 | 					};
436 | 				},
437 | 			},
438 | 		],
439 | 	} satisfies BetterAuthClientPlugin;
440 | };
441 | 
```
Page 35/71FirstPrevNextLast