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

# Directory Structure

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

# Files

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

```typescript
   1 | import * as z from "zod";
   2 | import {
   3 | 	createAuthEndpoint,
   4 | 	createAuthMiddleware,
   5 | } from "@better-auth/core/api";
   6 | import type { BetterAuthPlugin, BetterAuthOptions } from "@better-auth/core";
   7 | import {
   8 | 	oidcProvider,
   9 | 	type Client,
  10 | 	type CodeVerificationValue,
  11 | 	type OAuthAccessToken,
  12 | 	type OIDCMetadata,
  13 | 	type OIDCOptions,
  14 | } from "../oidc-provider";
  15 | import { APIError, getSessionFromCtx } from "../../api";
  16 | import { base64 } from "@better-auth/utils/base64";
  17 | import { generateRandomString } from "../../crypto";
  18 | import { createHash } from "@better-auth/utils/hash";
  19 | import { getWebcryptoSubtle } from "@better-auth/utils";
  20 | import { SignJWT } from "jose";
  21 | import { parseSetCookieHeader } from "../../cookies";
  22 | import { schema } from "../oidc-provider/schema";
  23 | import { authorizeMCPOAuth } from "./authorize";
  24 | import { getBaseURL } from "../../utils/url";
  25 | import { isProduction } from "@better-auth/core/env";
  26 | import { logger } from "@better-auth/core/env";
  27 | import type { GenericEndpointContext } from "@better-auth/core";
  28 | 
  29 | interface MCPOptions {
  30 | 	loginPage: string;
  31 | 	resource?: string;
  32 | 	oidcConfig?: OIDCOptions;
  33 | }
  34 | 
  35 | export const getMCPProviderMetadata = (
  36 | 	ctx: GenericEndpointContext,
  37 | 	options?: OIDCOptions,
  38 | ): OIDCMetadata => {
  39 | 	const issuer = ctx.context.options.baseURL as string;
  40 | 	const baseURL = ctx.context.baseURL;
  41 | 	if (!issuer || !baseURL) {
  42 | 		throw new APIError("INTERNAL_SERVER_ERROR", {
  43 | 			error: "invalid_issuer",
  44 | 			error_description:
  45 | 				"issuer or baseURL is not set. If you're the app developer, please make sure to set the `baseURL` in your auth config.",
  46 | 		});
  47 | 	}
  48 | 	return {
  49 | 		issuer,
  50 | 		authorization_endpoint: `${baseURL}/mcp/authorize`,
  51 | 		token_endpoint: `${baseURL}/mcp/token`,
  52 | 		userinfo_endpoint: `${baseURL}/mcp/userinfo`,
  53 | 		jwks_uri: `${baseURL}/mcp/jwks`,
  54 | 		registration_endpoint: `${baseURL}/mcp/register`,
  55 | 		scopes_supported: ["openid", "profile", "email", "offline_access"],
  56 | 		response_types_supported: ["code"],
  57 | 		response_modes_supported: ["query"],
  58 | 		grant_types_supported: ["authorization_code", "refresh_token"],
  59 | 		acr_values_supported: [
  60 | 			"urn:mace:incommon:iap:silver",
  61 | 			"urn:mace:incommon:iap:bronze",
  62 | 		],
  63 | 		subject_types_supported: ["public"],
  64 | 		id_token_signing_alg_values_supported: ["RS256", "none"],
  65 | 		token_endpoint_auth_methods_supported: [
  66 | 			"client_secret_basic",
  67 | 			"client_secret_post",
  68 | 			"none",
  69 | 		],
  70 | 		code_challenge_methods_supported: ["S256"],
  71 | 		claims_supported: [
  72 | 			"sub",
  73 | 			"iss",
  74 | 			"aud",
  75 | 			"exp",
  76 | 			"nbf",
  77 | 			"iat",
  78 | 			"jti",
  79 | 			"email",
  80 | 			"email_verified",
  81 | 			"name",
  82 | 		],
  83 | 		...options?.metadata,
  84 | 	};
  85 | };
  86 | 
  87 | export const getMCPProtectedResourceMetadata = (
  88 | 	ctx: GenericEndpointContext,
  89 | 	options?: MCPOptions,
  90 | ) => {
  91 | 	const baseURL = ctx.context.baseURL;
  92 | 
  93 | 	return {
  94 | 		resource: options?.resource ?? new URL(baseURL).origin,
  95 | 		authorization_servers: [baseURL],
  96 | 		jwks_uri: options?.oidcConfig?.metadata?.jwks_uri ?? `${baseURL}/mcp/jwks`,
  97 | 		scopes_supported: options?.oidcConfig?.metadata?.scopes_supported ?? [
  98 | 			"openid",
  99 | 			"profile",
 100 | 			"email",
 101 | 			"offline_access",
 102 | 		],
 103 | 		bearer_methods_supported: ["header"],
 104 | 		resource_signing_alg_values_supported: ["RS256", "none"],
 105 | 	};
 106 | };
 107 | 
 108 | export const mcp = (options: MCPOptions) => {
 109 | 	const opts = {
 110 | 		codeExpiresIn: 600,
 111 | 		defaultScope: "openid",
 112 | 		accessTokenExpiresIn: 3600,
 113 | 		refreshTokenExpiresIn: 604800,
 114 | 		allowPlainCodeChallengeMethod: true,
 115 | 		...options.oidcConfig,
 116 | 		loginPage: options.loginPage,
 117 | 		scopes: [
 118 | 			"openid",
 119 | 			"profile",
 120 | 			"email",
 121 | 			"offline_access",
 122 | 			...(options.oidcConfig?.scopes || []),
 123 | 		],
 124 | 	};
 125 | 	const modelName = {
 126 | 		oauthClient: "oauthApplication",
 127 | 		oauthAccessToken: "oauthAccessToken",
 128 | 		oauthConsent: "oauthConsent",
 129 | 	};
 130 | 	const provider = oidcProvider(opts);
 131 | 	return {
 132 | 		id: "mcp",
 133 | 		hooks: {
 134 | 			after: [
 135 | 				{
 136 | 					matcher() {
 137 | 						return true;
 138 | 					},
 139 | 					handler: createAuthMiddleware(async (ctx) => {
 140 | 						const cookie = await ctx.getSignedCookie(
 141 | 							"oidc_login_prompt",
 142 | 							ctx.context.secret,
 143 | 						);
 144 | 						const cookieName = ctx.context.authCookies.sessionToken.name;
 145 | 						const parsedSetCookieHeader = parseSetCookieHeader(
 146 | 							ctx.context.responseHeaders?.get("set-cookie") || "",
 147 | 						);
 148 | 						const hasSessionToken = parsedSetCookieHeader.has(cookieName);
 149 | 						if (!cookie || !hasSessionToken) {
 150 | 							return;
 151 | 						}
 152 | 						ctx.setCookie("oidc_login_prompt", "", {
 153 | 							maxAge: 0,
 154 | 						});
 155 | 						const sessionCookie = parsedSetCookieHeader.get(cookieName)?.value;
 156 | 						const sessionToken = sessionCookie?.split(".")[0]!;
 157 | 						if (!sessionToken) {
 158 | 							return;
 159 | 						}
 160 | 						const session =
 161 | 							await ctx.context.internalAdapter.findSession(sessionToken);
 162 | 						if (!session) {
 163 | 							return;
 164 | 						}
 165 | 						ctx.query = JSON.parse(cookie);
 166 | 						ctx.query!.prompt = "consent";
 167 | 						ctx.context.session = session;
 168 | 						const response = await authorizeMCPOAuth(ctx, opts);
 169 | 						return response;
 170 | 					}),
 171 | 				},
 172 | 			],
 173 | 		},
 174 | 		endpoints: {
 175 | 			getMcpOAuthConfig: createAuthEndpoint(
 176 | 				"/.well-known/oauth-authorization-server",
 177 | 				{
 178 | 					method: "GET",
 179 | 					metadata: {
 180 | 						client: false,
 181 | 					},
 182 | 				},
 183 | 				async (c) => {
 184 | 					try {
 185 | 						const metadata = getMCPProviderMetadata(c, options);
 186 | 						return c.json(metadata);
 187 | 					} catch (e) {
 188 | 						console.log(e);
 189 | 						return c.json(null);
 190 | 					}
 191 | 				},
 192 | 			),
 193 | 			getMCPProtectedResource: createAuthEndpoint(
 194 | 				"/.well-known/oauth-protected-resource",
 195 | 				{
 196 | 					method: "GET",
 197 | 					metadata: {
 198 | 						client: false,
 199 | 					},
 200 | 				},
 201 | 				async (c) => {
 202 | 					const metadata = getMCPProtectedResourceMetadata(c, options);
 203 | 					return c.json(metadata);
 204 | 				},
 205 | 			),
 206 | 			mcpOAuthAuthorize: createAuthEndpoint(
 207 | 				"/mcp/authorize",
 208 | 				{
 209 | 					method: "GET",
 210 | 					query: z.record(z.string(), z.any()),
 211 | 					metadata: {
 212 | 						openapi: {
 213 | 							description: "Authorize an OAuth2 request using MCP",
 214 | 							responses: {
 215 | 								"200": {
 216 | 									description: "Authorization response generated successfully",
 217 | 									content: {
 218 | 										"application/json": {
 219 | 											schema: {
 220 | 												type: "object",
 221 | 												additionalProperties: true,
 222 | 												description:
 223 | 													"Authorization response, contents depend on the authorize function implementation",
 224 | 											},
 225 | 										},
 226 | 									},
 227 | 								},
 228 | 							},
 229 | 						},
 230 | 					},
 231 | 				},
 232 | 				async (ctx) => {
 233 | 					return authorizeMCPOAuth(ctx, opts);
 234 | 				},
 235 | 			),
 236 | 			mcpOAuthToken: createAuthEndpoint(
 237 | 				"/mcp/token",
 238 | 				{
 239 | 					method: "POST",
 240 | 					body: z.record(z.any(), z.any()),
 241 | 					metadata: {
 242 | 						isAction: false,
 243 | 					},
 244 | 				},
 245 | 				async (ctx) => {
 246 | 					//cors
 247 | 					ctx.setHeader("Access-Control-Allow-Origin", "*");
 248 | 					ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
 249 | 					ctx.setHeader(
 250 | 						"Access-Control-Allow-Headers",
 251 | 						"Content-Type, Authorization",
 252 | 					);
 253 | 					ctx.setHeader("Access-Control-Max-Age", "86400");
 254 | 
 255 | 					let { body } = ctx;
 256 | 					if (!body) {
 257 | 						throw ctx.error("BAD_REQUEST", {
 258 | 							error_description: "request body not found",
 259 | 							error: "invalid_request",
 260 | 						});
 261 | 					}
 262 | 					if (body instanceof FormData) {
 263 | 						body = Object.fromEntries(body.entries());
 264 | 					}
 265 | 					if (!(body instanceof Object)) {
 266 | 						throw new APIError("BAD_REQUEST", {
 267 | 							error_description: "request body is not an object",
 268 | 							error: "invalid_request",
 269 | 						});
 270 | 					}
 271 | 					let { client_id, client_secret } = body;
 272 | 					const authorization =
 273 | 						ctx.request?.headers.get("authorization") || null;
 274 | 					if (
 275 | 						authorization &&
 276 | 						!client_id &&
 277 | 						!client_secret &&
 278 | 						authorization.startsWith("Basic ")
 279 | 					) {
 280 | 						try {
 281 | 							const encoded = authorization.replace("Basic ", "");
 282 | 							const decoded = new TextDecoder().decode(base64.decode(encoded));
 283 | 							if (!decoded.includes(":")) {
 284 | 								throw new APIError("UNAUTHORIZED", {
 285 | 									error_description: "invalid authorization header format",
 286 | 									error: "invalid_client",
 287 | 								});
 288 | 							}
 289 | 							const [id, secret] = decoded.split(":");
 290 | 							if (!id || !secret) {
 291 | 								throw new APIError("UNAUTHORIZED", {
 292 | 									error_description: "invalid authorization header format",
 293 | 									error: "invalid_client",
 294 | 								});
 295 | 							}
 296 | 							client_id = id;
 297 | 							client_secret = secret;
 298 | 						} catch (error) {
 299 | 							throw new APIError("UNAUTHORIZED", {
 300 | 								error_description: "invalid authorization header format",
 301 | 								error: "invalid_client",
 302 | 							});
 303 | 						}
 304 | 					}
 305 | 					const {
 306 | 						grant_type,
 307 | 						code,
 308 | 						redirect_uri,
 309 | 						refresh_token,
 310 | 						code_verifier,
 311 | 					} = body;
 312 | 					if (grant_type === "refresh_token") {
 313 | 						if (!refresh_token) {
 314 | 							throw new APIError("BAD_REQUEST", {
 315 | 								error_description: "refresh_token is required",
 316 | 								error: "invalid_request",
 317 | 							});
 318 | 						}
 319 | 						const token = await ctx.context.adapter.findOne<OAuthAccessToken>({
 320 | 							model: "oauthAccessToken",
 321 | 							where: [
 322 | 								{
 323 | 									field: "refreshToken",
 324 | 									value: refresh_token.toString(),
 325 | 								},
 326 | 							],
 327 | 						});
 328 | 						if (!token) {
 329 | 							throw new APIError("UNAUTHORIZED", {
 330 | 								error_description: "invalid refresh token",
 331 | 								error: "invalid_grant",
 332 | 							});
 333 | 						}
 334 | 						if (token.clientId !== client_id?.toString()) {
 335 | 							throw new APIError("UNAUTHORIZED", {
 336 | 								error_description: "invalid client_id",
 337 | 								error: "invalid_client",
 338 | 							});
 339 | 						}
 340 | 						if (token.refreshTokenExpiresAt < new Date()) {
 341 | 							throw new APIError("UNAUTHORIZED", {
 342 | 								error_description: "refresh token expired",
 343 | 								error: "invalid_grant",
 344 | 							});
 345 | 						}
 346 | 						const accessToken = generateRandomString(32, "a-z", "A-Z");
 347 | 						const newRefreshToken = generateRandomString(32, "a-z", "A-Z");
 348 | 						const accessTokenExpiresAt = new Date(
 349 | 							Date.now() + opts.accessTokenExpiresIn * 1000,
 350 | 						);
 351 | 						const refreshTokenExpiresAt = new Date(
 352 | 							Date.now() + opts.refreshTokenExpiresIn * 1000,
 353 | 						);
 354 | 						await ctx.context.adapter.create({
 355 | 							model: modelName.oauthAccessToken,
 356 | 							data: {
 357 | 								accessToken,
 358 | 								refreshToken: newRefreshToken,
 359 | 								accessTokenExpiresAt,
 360 | 								refreshTokenExpiresAt,
 361 | 								clientId: client_id.toString(),
 362 | 								userId: token.userId,
 363 | 								scopes: token.scopes,
 364 | 								createdAt: new Date(),
 365 | 								updatedAt: new Date(),
 366 | 							},
 367 | 						});
 368 | 						return ctx.json({
 369 | 							access_token: accessToken,
 370 | 							token_type: "bearer",
 371 | 							expires_in: opts.accessTokenExpiresIn,
 372 | 							refresh_token: newRefreshToken,
 373 | 							scope: token.scopes,
 374 | 						});
 375 | 					}
 376 | 
 377 | 					if (!code) {
 378 | 						throw new APIError("BAD_REQUEST", {
 379 | 							error_description: "code is required",
 380 | 							error: "invalid_request",
 381 | 						});
 382 | 					}
 383 | 
 384 | 					if (opts.requirePKCE && !code_verifier) {
 385 | 						throw new APIError("BAD_REQUEST", {
 386 | 							error_description: "code verifier is missing",
 387 | 							error: "invalid_request",
 388 | 						});
 389 | 					}
 390 | 
 391 | 					/**
 392 | 					 * We need to check if the code is valid before we can proceed
 393 | 					 * with the rest of the request.
 394 | 					 */
 395 | 					const verificationValue =
 396 | 						await ctx.context.internalAdapter.findVerificationValue(
 397 | 							code.toString(),
 398 | 						);
 399 | 					if (!verificationValue) {
 400 | 						throw new APIError("UNAUTHORIZED", {
 401 | 							error_description: "invalid code",
 402 | 							error: "invalid_grant",
 403 | 						});
 404 | 					}
 405 | 					if (verificationValue.expiresAt < new Date()) {
 406 | 						throw new APIError("UNAUTHORIZED", {
 407 | 							error_description: "code expired",
 408 | 							error: "invalid_grant",
 409 | 						});
 410 | 					}
 411 | 
 412 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 413 | 						verificationValue.id,
 414 | 					);
 415 | 
 416 | 					if (!client_id) {
 417 | 						throw new APIError("UNAUTHORIZED", {
 418 | 							error_description: "client_id is required",
 419 | 							error: "invalid_client",
 420 | 						});
 421 | 					}
 422 | 					if (!grant_type) {
 423 | 						throw new APIError("BAD_REQUEST", {
 424 | 							error_description: "grant_type is required",
 425 | 							error: "invalid_request",
 426 | 						});
 427 | 					}
 428 | 					if (grant_type !== "authorization_code") {
 429 | 						throw new APIError("BAD_REQUEST", {
 430 | 							error_description: "grant_type must be 'authorization_code'",
 431 | 							error: "unsupported_grant_type",
 432 | 						});
 433 | 					}
 434 | 
 435 | 					if (!redirect_uri) {
 436 | 						throw new APIError("BAD_REQUEST", {
 437 | 							error_description: "redirect_uri is required",
 438 | 							error: "invalid_request",
 439 | 						});
 440 | 					}
 441 | 
 442 | 					const client = await ctx.context.adapter
 443 | 						.findOne<Record<string, any>>({
 444 | 							model: modelName.oauthClient,
 445 | 							where: [{ field: "clientId", value: client_id.toString() }],
 446 | 						})
 447 | 						.then((res) => {
 448 | 							if (!res) {
 449 | 								return null;
 450 | 							}
 451 | 							return {
 452 | 								...res,
 453 | 								redirectURLs: res.redirectURLs.split(","),
 454 | 								metadata: res.metadata ? JSON.parse(res.metadata) : {},
 455 | 							} as Client;
 456 | 						});
 457 | 					if (!client) {
 458 | 						throw new APIError("UNAUTHORIZED", {
 459 | 							error_description: "invalid client_id",
 460 | 							error: "invalid_client",
 461 | 						});
 462 | 					}
 463 | 					if (client.disabled) {
 464 | 						throw new APIError("UNAUTHORIZED", {
 465 | 							error_description: "client is disabled",
 466 | 							error: "invalid_client",
 467 | 						});
 468 | 					}
 469 | 					// For public clients (type: 'public'), validate PKCE instead of client_secret
 470 | 					if (client.type === "public") {
 471 | 						// Public clients must use PKCE
 472 | 						if (!code_verifier) {
 473 | 							throw new APIError("BAD_REQUEST", {
 474 | 								error_description:
 475 | 									"code verifier is required for public clients",
 476 | 								error: "invalid_request",
 477 | 							});
 478 | 						}
 479 | 						// PKCE validation happens later in the flow, so we skip client_secret validation
 480 | 					} else {
 481 | 						// For confidential clients, validate client_secret
 482 | 						if (!client_secret) {
 483 | 							throw new APIError("UNAUTHORIZED", {
 484 | 								error_description:
 485 | 									"client_secret is required for confidential clients",
 486 | 								error: "invalid_client",
 487 | 							});
 488 | 						}
 489 | 						const isValidSecret =
 490 | 							client.clientSecret === client_secret.toString();
 491 | 						if (!isValidSecret) {
 492 | 							throw new APIError("UNAUTHORIZED", {
 493 | 								error_description: "invalid client_secret",
 494 | 								error: "invalid_client",
 495 | 							});
 496 | 						}
 497 | 					}
 498 | 					const value = JSON.parse(
 499 | 						verificationValue.value,
 500 | 					) as CodeVerificationValue;
 501 | 					if (value.clientId !== client_id.toString()) {
 502 | 						throw new APIError("UNAUTHORIZED", {
 503 | 							error_description: "invalid client_id",
 504 | 							error: "invalid_client",
 505 | 						});
 506 | 					}
 507 | 					if (value.redirectURI !== redirect_uri.toString()) {
 508 | 						throw new APIError("UNAUTHORIZED", {
 509 | 							error_description: "invalid redirect_uri",
 510 | 							error: "invalid_client",
 511 | 						});
 512 | 					}
 513 | 					if (value.codeChallenge && !code_verifier) {
 514 | 						throw new APIError("BAD_REQUEST", {
 515 | 							error_description: "code verifier is missing",
 516 | 							error: "invalid_request",
 517 | 						});
 518 | 					}
 519 | 
 520 | 					const challenge =
 521 | 						value.codeChallengeMethod === "plain"
 522 | 							? code_verifier
 523 | 							: await createHash("SHA-256", "base64urlnopad").digest(
 524 | 									code_verifier,
 525 | 								);
 526 | 
 527 | 					if (challenge !== value.codeChallenge) {
 528 | 						throw new APIError("UNAUTHORIZED", {
 529 | 							error_description: "code verification failed",
 530 | 							error: "invalid_request",
 531 | 						});
 532 | 					}
 533 | 
 534 | 					const requestedScopes = value.scope;
 535 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 536 | 						verificationValue.id,
 537 | 					);
 538 | 					const accessToken = generateRandomString(32, "a-z", "A-Z");
 539 | 					const refreshToken = generateRandomString(32, "A-Z", "a-z");
 540 | 					const accessTokenExpiresAt = new Date(
 541 | 						Date.now() + opts.accessTokenExpiresIn * 1000,
 542 | 					);
 543 | 					const refreshTokenExpiresAt = new Date(
 544 | 						Date.now() + opts.refreshTokenExpiresIn * 1000,
 545 | 					);
 546 | 					await ctx.context.adapter.create({
 547 | 						model: modelName.oauthAccessToken,
 548 | 						data: {
 549 | 							accessToken,
 550 | 							refreshToken,
 551 | 							accessTokenExpiresAt,
 552 | 							refreshTokenExpiresAt,
 553 | 							clientId: client_id.toString(),
 554 | 							userId: value.userId,
 555 | 							scopes: requestedScopes.join(" "),
 556 | 							createdAt: new Date(),
 557 | 							updatedAt: new Date(),
 558 | 						},
 559 | 					});
 560 | 					const user = await ctx.context.internalAdapter.findUserById(
 561 | 						value.userId,
 562 | 					);
 563 | 					if (!user) {
 564 | 						throw new APIError("UNAUTHORIZED", {
 565 | 							error_description: "user not found",
 566 | 							error: "invalid_grant",
 567 | 						});
 568 | 					}
 569 | 					let secretKey = {
 570 | 						alg: "HS256",
 571 | 						key: await getWebcryptoSubtle().generateKey(
 572 | 							{
 573 | 								name: "HMAC",
 574 | 								hash: "SHA-256",
 575 | 							},
 576 | 							true,
 577 | 							["sign", "verify"],
 578 | 						),
 579 | 					};
 580 | 					const profile = {
 581 | 						given_name: user.name.split(" ")[0]!,
 582 | 						family_name: user.name.split(" ")[1]!,
 583 | 						name: user.name,
 584 | 						profile: user.image,
 585 | 						updated_at: user.updatedAt.toISOString(),
 586 | 					};
 587 | 					const email = {
 588 | 						email: user.email,
 589 | 						email_verified: user.emailVerified,
 590 | 					};
 591 | 					const userClaims = {
 592 | 						...(requestedScopes.includes("profile") ? profile : {}),
 593 | 						...(requestedScopes.includes("email") ? email : {}),
 594 | 					};
 595 | 
 596 | 					const additionalUserClaims = opts.getAdditionalUserInfoClaim
 597 | 						? await opts.getAdditionalUserInfoClaim(
 598 | 								user,
 599 | 								requestedScopes,
 600 | 								client,
 601 | 							)
 602 | 						: {};
 603 | 
 604 | 					const idToken = await new SignJWT({
 605 | 						sub: user.id,
 606 | 						aud: client_id.toString(),
 607 | 						iat: Date.now(),
 608 | 						auth_time: ctx.context.session
 609 | 							? new Date(ctx.context.session.session.createdAt).getTime()
 610 | 							: undefined,
 611 | 						nonce: value.nonce,
 612 | 						acr: "urn:mace:incommon:iap:silver", // default to silver - ⚠︎ this should be configurable and should be validated against the client's metadata
 613 | 						...userClaims,
 614 | 						...additionalUserClaims,
 615 | 					})
 616 | 						.setProtectedHeader({ alg: secretKey.alg })
 617 | 						.setIssuedAt()
 618 | 						.setExpirationTime(
 619 | 							Math.floor(Date.now() / 1000) + opts.accessTokenExpiresIn,
 620 | 						)
 621 | 						.sign(secretKey.key);
 622 | 					return ctx.json(
 623 | 						{
 624 | 							access_token: accessToken,
 625 | 							token_type: "Bearer",
 626 | 							expires_in: opts.accessTokenExpiresIn,
 627 | 							refresh_token: requestedScopes.includes("offline_access")
 628 | 								? refreshToken
 629 | 								: undefined,
 630 | 							scope: requestedScopes.join(" "),
 631 | 							id_token: requestedScopes.includes("openid")
 632 | 								? idToken
 633 | 								: undefined,
 634 | 						},
 635 | 						{
 636 | 							headers: {
 637 | 								"Cache-Control": "no-store",
 638 | 								Pragma: "no-cache",
 639 | 							},
 640 | 						},
 641 | 					);
 642 | 				},
 643 | 			),
 644 | 			registerMcpClient: createAuthEndpoint(
 645 | 				"/mcp/register",
 646 | 				{
 647 | 					method: "POST",
 648 | 					body: z.object({
 649 | 						redirect_uris: z.array(z.string()),
 650 | 						token_endpoint_auth_method: z
 651 | 							.enum(["none", "client_secret_basic", "client_secret_post"])
 652 | 							.default("client_secret_basic")
 653 | 							.optional(),
 654 | 						grant_types: z
 655 | 							.array(
 656 | 								z.enum([
 657 | 									"authorization_code",
 658 | 									"implicit",
 659 | 									"password",
 660 | 									"client_credentials",
 661 | 									"refresh_token",
 662 | 									"urn:ietf:params:oauth:grant-type:jwt-bearer",
 663 | 									"urn:ietf:params:oauth:grant-type:saml2-bearer",
 664 | 								]),
 665 | 							)
 666 | 							.default(["authorization_code"])
 667 | 							.optional(),
 668 | 						response_types: z
 669 | 							.array(z.enum(["code", "token"]))
 670 | 							.default(["code"])
 671 | 							.optional(),
 672 | 						client_name: z.string().optional(),
 673 | 						client_uri: z.string().optional(),
 674 | 						logo_uri: z.string().optional(),
 675 | 						scope: z.string().optional(),
 676 | 						contacts: z.array(z.string()).optional(),
 677 | 						tos_uri: z.string().optional(),
 678 | 						policy_uri: z.string().optional(),
 679 | 						jwks_uri: z.string().optional(),
 680 | 						jwks: z.record(z.string(), z.any()).optional(),
 681 | 						metadata: z.record(z.any(), z.any()).optional(),
 682 | 						software_id: z.string().optional(),
 683 | 						software_version: z.string().optional(),
 684 | 						software_statement: z.string().optional(),
 685 | 					}),
 686 | 					metadata: {
 687 | 						openapi: {
 688 | 							description: "Register an OAuth2 application",
 689 | 							responses: {
 690 | 								"200": {
 691 | 									description: "OAuth2 application registered successfully",
 692 | 									content: {
 693 | 										"application/json": {
 694 | 											schema: {
 695 | 												type: "object",
 696 | 												properties: {
 697 | 													name: {
 698 | 														type: "string",
 699 | 														description: "Name of the OAuth2 application",
 700 | 													},
 701 | 													icon: {
 702 | 														type: "string",
 703 | 														nullable: true,
 704 | 														description: "Icon URL for the application",
 705 | 													},
 706 | 													metadata: {
 707 | 														type: "object",
 708 | 														additionalProperties: true,
 709 | 														nullable: true,
 710 | 														description:
 711 | 															"Additional metadata for the application",
 712 | 													},
 713 | 													clientId: {
 714 | 														type: "string",
 715 | 														description: "Unique identifier for the client",
 716 | 													},
 717 | 													clientSecret: {
 718 | 														type: "string",
 719 | 														description:
 720 | 															"Secret key for the client. Not included for public clients.",
 721 | 													},
 722 | 													redirectURLs: {
 723 | 														type: "array",
 724 | 														items: { type: "string", format: "uri" },
 725 | 														description: "List of allowed redirect URLs",
 726 | 													},
 727 | 													type: {
 728 | 														type: "string",
 729 | 														description: "Type of the client",
 730 | 														enum: ["web", "public"],
 731 | 													},
 732 | 													authenticationScheme: {
 733 | 														type: "string",
 734 | 														description:
 735 | 															"Authentication scheme used by the client",
 736 | 														enum: ["client_secret", "none"],
 737 | 													},
 738 | 													disabled: {
 739 | 														type: "boolean",
 740 | 														description: "Whether the client is disabled",
 741 | 														enum: [false],
 742 | 													},
 743 | 													userId: {
 744 | 														type: "string",
 745 | 														nullable: true,
 746 | 														description:
 747 | 															"ID of the user who registered the client, null if registered anonymously",
 748 | 													},
 749 | 													createdAt: {
 750 | 														type: "string",
 751 | 														format: "date-time",
 752 | 														description: "Creation timestamp",
 753 | 													},
 754 | 													updatedAt: {
 755 | 														type: "string",
 756 | 														format: "date-time",
 757 | 														description: "Last update timestamp",
 758 | 													},
 759 | 												},
 760 | 												required: [
 761 | 													"name",
 762 | 													"clientId",
 763 | 													"redirectURLs",
 764 | 													"type",
 765 | 													"authenticationScheme",
 766 | 													"disabled",
 767 | 													"createdAt",
 768 | 													"updatedAt",
 769 | 												],
 770 | 											},
 771 | 										},
 772 | 									},
 773 | 								},
 774 | 							},
 775 | 						},
 776 | 					},
 777 | 				},
 778 | 				async (ctx) => {
 779 | 					const body = ctx.body;
 780 | 					const session = await getSessionFromCtx(ctx);
 781 | 					ctx.setHeader("Access-Control-Allow-Origin", "*");
 782 | 					ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
 783 | 					ctx.setHeader(
 784 | 						"Access-Control-Allow-Headers",
 785 | 						"Content-Type, Authorization",
 786 | 					);
 787 | 					ctx.setHeader("Access-Control-Max-Age", "86400");
 788 | 					ctx.headers?.set("Access-Control-Max-Age", "86400");
 789 | 					if (
 790 | 						(!body.grant_types ||
 791 | 							body.grant_types.includes("authorization_code") ||
 792 | 							body.grant_types.includes("implicit")) &&
 793 | 						(!body.redirect_uris || body.redirect_uris.length === 0)
 794 | 					) {
 795 | 						throw new APIError("BAD_REQUEST", {
 796 | 							error: "invalid_redirect_uri",
 797 | 							error_description:
 798 | 								"Redirect URIs are required for authorization_code and implicit grant types",
 799 | 						});
 800 | 					}
 801 | 
 802 | 					if (body.grant_types && body.response_types) {
 803 | 						if (
 804 | 							body.grant_types.includes("authorization_code") &&
 805 | 							!body.response_types.includes("code")
 806 | 						) {
 807 | 							throw new APIError("BAD_REQUEST", {
 808 | 								error: "invalid_client_metadata",
 809 | 								error_description:
 810 | 									"When 'authorization_code' grant type is used, 'code' response type must be included",
 811 | 							});
 812 | 						}
 813 | 						if (
 814 | 							body.grant_types.includes("implicit") &&
 815 | 							!body.response_types.includes("token")
 816 | 						) {
 817 | 							throw new APIError("BAD_REQUEST", {
 818 | 								error: "invalid_client_metadata",
 819 | 								error_description:
 820 | 									"When 'implicit' grant type is used, 'token' response type must be included",
 821 | 							});
 822 | 						}
 823 | 					}
 824 | 
 825 | 					const clientId =
 826 | 						opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
 827 | 					const clientSecret =
 828 | 						opts.generateClientSecret?.() ||
 829 | 						generateRandomString(32, "a-z", "A-Z");
 830 | 
 831 | 					// Determine client type based on auth method
 832 | 					const clientType =
 833 | 						body.token_endpoint_auth_method === "none" ? "public" : "web";
 834 | 					const finalClientSecret = clientType === "public" ? "" : clientSecret;
 835 | 
 836 | 					await ctx.context.adapter.create({
 837 | 						model: modelName.oauthClient,
 838 | 						data: {
 839 | 							name: body.client_name,
 840 | 							icon: body.logo_uri,
 841 | 							metadata: body.metadata ? JSON.stringify(body.metadata) : null,
 842 | 							clientId: clientId,
 843 | 							clientSecret: finalClientSecret,
 844 | 							redirectURLs: body.redirect_uris.join(","),
 845 | 							type: clientType,
 846 | 							authenticationScheme:
 847 | 								body.token_endpoint_auth_method || "client_secret_basic",
 848 | 							disabled: false,
 849 | 							userId: session?.session.userId,
 850 | 							createdAt: new Date(),
 851 | 							updatedAt: new Date(),
 852 | 						},
 853 | 					});
 854 | 
 855 | 					const responseData = {
 856 | 						client_id: clientId,
 857 | 						client_id_issued_at: Math.floor(Date.now() / 1000),
 858 | 						redirect_uris: body.redirect_uris,
 859 | 						token_endpoint_auth_method:
 860 | 							body.token_endpoint_auth_method || "client_secret_basic",
 861 | 						grant_types: body.grant_types || ["authorization_code"],
 862 | 						response_types: body.response_types || ["code"],
 863 | 						client_name: body.client_name,
 864 | 						client_uri: body.client_uri,
 865 | 						logo_uri: body.logo_uri,
 866 | 						scope: body.scope,
 867 | 						contacts: body.contacts,
 868 | 						tos_uri: body.tos_uri,
 869 | 						policy_uri: body.policy_uri,
 870 | 						jwks_uri: body.jwks_uri,
 871 | 						jwks: body.jwks,
 872 | 						software_id: body.software_id,
 873 | 						software_version: body.software_version,
 874 | 						software_statement: body.software_statement,
 875 | 						metadata: body.metadata,
 876 | 						...(clientType !== "public"
 877 | 							? {
 878 | 									client_secret: finalClientSecret,
 879 | 									client_secret_expires_at: 0, // 0 means it doesn't expire
 880 | 								}
 881 | 							: {}),
 882 | 					};
 883 | 
 884 | 					return new Response(JSON.stringify(responseData), {
 885 | 						status: 201,
 886 | 						headers: {
 887 | 							"Content-Type": "application/json",
 888 | 							"Cache-Control": "no-store",
 889 | 							Pragma: "no-cache",
 890 | 						},
 891 | 					});
 892 | 				},
 893 | 			),
 894 | 			getMcpSession: createAuthEndpoint(
 895 | 				"/mcp/get-session",
 896 | 				{
 897 | 					method: "GET",
 898 | 					requireHeaders: true,
 899 | 				},
 900 | 				async (c) => {
 901 | 					const accessToken = c.headers
 902 | 						?.get("Authorization")
 903 | 						?.replace("Bearer ", "");
 904 | 					if (!accessToken) {
 905 | 						c.headers?.set("WWW-Authenticate", "Bearer");
 906 | 						return c.json(null);
 907 | 					}
 908 | 					const accessTokenData =
 909 | 						await c.context.adapter.findOne<OAuthAccessToken>({
 910 | 							model: modelName.oauthAccessToken,
 911 | 							where: [
 912 | 								{
 913 | 									field: "accessToken",
 914 | 									value: accessToken,
 915 | 								},
 916 | 							],
 917 | 						});
 918 | 					if (!accessTokenData) {
 919 | 						return c.json(null);
 920 | 					}
 921 | 					return c.json(accessTokenData);
 922 | 				},
 923 | 			),
 924 | 		},
 925 | 		schema,
 926 | 	} satisfies BetterAuthPlugin;
 927 | };
 928 | 
 929 | export const withMcpAuth = <
 930 | 	Auth extends {
 931 | 		api: {
 932 | 			getMcpSession: (...args: any) => Promise<OAuthAccessToken | null>;
 933 | 		};
 934 | 		options: BetterAuthOptions;
 935 | 	},
 936 | >(
 937 | 	auth: Auth,
 938 | 	handler: (
 939 | 		req: Request,
 940 | 		sesssion: OAuthAccessToken,
 941 | 	) => Response | Promise<Response>,
 942 | ) => {
 943 | 	return async (req: Request) => {
 944 | 		const baseURL = getBaseURL(auth.options.baseURL, auth.options.basePath);
 945 | 		if (!baseURL && !isProduction) {
 946 | 			logger.warn("Unable to get the baseURL, please check your config!");
 947 | 		}
 948 | 		const session = await auth.api.getMcpSession({
 949 | 			headers: req.headers,
 950 | 		});
 951 | 		const wwwAuthenticateValue = `Bearer resource_metadata="${baseURL}/.well-known/oauth-protected-resource"`;
 952 | 		if (!session) {
 953 | 			return Response.json(
 954 | 				{
 955 | 					jsonrpc: "2.0",
 956 | 					error: {
 957 | 						code: -32000,
 958 | 						message: "Unauthorized: Authentication required",
 959 | 						"www-authenticate": wwwAuthenticateValue,
 960 | 					},
 961 | 					id: null,
 962 | 				},
 963 | 				{
 964 | 					status: 401,
 965 | 					headers: {
 966 | 						"WWW-Authenticate": wwwAuthenticateValue,
 967 | 						// we also add this headers otherwise browser based clients will not be able to read the `www-authenticate` header
 968 | 						"Access-Control-Expose-Headers": "WWW-Authenticate",
 969 | 					},
 970 | 				},
 971 | 			);
 972 | 		}
 973 | 		return handler(req, session);
 974 | 	};
 975 | };
 976 | 
 977 | export const oAuthDiscoveryMetadata = <
 978 | 	Auth extends {
 979 | 		api: {
 980 | 			getMcpOAuthConfig: (...args: any) => any;
 981 | 		};
 982 | 	},
 983 | >(
 984 | 	auth: Auth,
 985 | ) => {
 986 | 	return async (request: Request) => {
 987 | 		const res = await auth.api.getMcpOAuthConfig();
 988 | 		return new Response(JSON.stringify(res), {
 989 | 			status: 200,
 990 | 			headers: {
 991 | 				"Content-Type": "application/json",
 992 | 				"Access-Control-Allow-Origin": "*",
 993 | 				"Access-Control-Allow-Methods": "POST, OPTIONS",
 994 | 				"Access-Control-Allow-Headers": "Content-Type, Authorization",
 995 | 				"Access-Control-Max-Age": "86400",
 996 | 			},
 997 | 		});
 998 | 	};
 999 | };
1000 | 
1001 | export const oAuthProtectedResourceMetadata = <
1002 | 	Auth extends {
1003 | 		api: {
1004 | 			getMCPProtectedResource: (...args: any) => any;
1005 | 		};
1006 | 	},
1007 | >(
1008 | 	auth: Auth,
1009 | ) => {
1010 | 	return async (request: Request) => {
1011 | 		const res = await auth.api.getMCPProtectedResource();
1012 | 		return new Response(JSON.stringify(res), {
1013 | 			status: 200,
1014 | 			headers: {
1015 | 				"Content-Type": "application/json",
1016 | 				"Access-Control-Allow-Origin": "*",
1017 | 				"Access-Control-Allow-Methods": "POST, OPTIONS",
1018 | 				"Access-Control-Allow-Headers": "Content-Type, Authorization",
1019 | 				"Access-Control-Max-Age": "86400",
1020 | 			},
1021 | 		});
1022 | 	};
1023 | };
1024 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/commands/init.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { parse } from "dotenv";
   2 | import semver from "semver";
   3 | import { format as prettierFormat } from "prettier";
   4 | import { Command } from "commander";
   5 | import * as z from "zod/v4";
   6 | import { existsSync } from "fs";
   7 | import path from "path";
   8 | import fs from "fs/promises";
   9 | import { getPackageInfo } from "../utils/get-package-info";
  10 | import chalk from "chalk";
  11 | import {
  12 | 	cancel,
  13 | 	confirm,
  14 | 	intro,
  15 | 	isCancel,
  16 | 	log,
  17 | 	multiselect,
  18 | 	outro,
  19 | 	select,
  20 | 	spinner,
  21 | 	text,
  22 | } from "@clack/prompts";
  23 | import { installDependencies } from "../utils/install-dependencies";
  24 | import { checkPackageManagers } from "../utils/check-package-managers";
  25 | import { formatMilliseconds } from "../utils/format-ms";
  26 | import { generateSecretHash } from "./secret";
  27 | import { generateAuthConfig } from "../generators/auth-config";
  28 | import { getTsconfigInfo } from "../utils/get-tsconfig-info";
  29 | 
  30 | /**
  31 |  * Should only use any database that is core DBs, and supports the Better Auth CLI generate functionality.
  32 |  */
  33 | const supportedDatabases = [
  34 | 	// Built-in kysely
  35 | 	"sqlite",
  36 | 	"mysql",
  37 | 	"mssql",
  38 | 	"postgres",
  39 | 	// Drizzle
  40 | 	"drizzle:pg",
  41 | 	"drizzle:mysql",
  42 | 	"drizzle:sqlite",
  43 | 	// Prisma
  44 | 	"prisma:postgresql",
  45 | 	"prisma:mysql",
  46 | 	"prisma:sqlite",
  47 | 	// Mongo
  48 | 	"mongodb",
  49 | ] as const;
  50 | 
  51 | export type SupportedDatabases = (typeof supportedDatabases)[number];
  52 | 
  53 | export const supportedPlugins = [
  54 | 	{
  55 | 		id: "two-factor",
  56 | 		name: "twoFactor",
  57 | 		path: `better-auth/plugins`,
  58 | 		clientName: "twoFactorClient",
  59 | 		clientPath: "better-auth/client/plugins",
  60 | 	},
  61 | 	{
  62 | 		id: "username",
  63 | 		name: "username",
  64 | 		clientName: "usernameClient",
  65 | 		path: `better-auth/plugins`,
  66 | 		clientPath: "better-auth/client/plugins",
  67 | 	},
  68 | 	{
  69 | 		id: "anonymous",
  70 | 		name: "anonymous",
  71 | 		clientName: "anonymousClient",
  72 | 		path: `better-auth/plugins`,
  73 | 		clientPath: "better-auth/client/plugins",
  74 | 	},
  75 | 	{
  76 | 		id: "phone-number",
  77 | 		name: "phoneNumber",
  78 | 		clientName: "phoneNumberClient",
  79 | 		path: `better-auth/plugins`,
  80 | 		clientPath: "better-auth/client/plugins",
  81 | 	},
  82 | 	{
  83 | 		id: "magic-link",
  84 | 		name: "magicLink",
  85 | 		clientName: "magicLinkClient",
  86 | 		clientPath: "better-auth/client/plugins",
  87 | 		path: `better-auth/plugins`,
  88 | 	},
  89 | 	{
  90 | 		id: "email-otp",
  91 | 		name: "emailOTP",
  92 | 		clientName: "emailOTPClient",
  93 | 		path: `better-auth/plugins`,
  94 | 		clientPath: "better-auth/client/plugins",
  95 | 	},
  96 | 	{
  97 | 		id: "passkey",
  98 | 		name: "passkey",
  99 | 		clientName: "passkeyClient",
 100 | 		path: `better-auth/plugins/passkey`,
 101 | 		clientPath: "better-auth/client/plugins",
 102 | 	},
 103 | 	{
 104 | 		id: "generic-oauth",
 105 | 		name: "genericOAuth",
 106 | 		clientName: "genericOAuthClient",
 107 | 		path: `better-auth/plugins`,
 108 | 		clientPath: "better-auth/client/plugins",
 109 | 	},
 110 | 	{
 111 | 		id: "one-tap",
 112 | 		name: "oneTap",
 113 | 		clientName: "oneTapClient",
 114 | 		path: `better-auth/plugins`,
 115 | 		clientPath: "better-auth/client/plugins",
 116 | 	},
 117 | 	{
 118 | 		id: "api-key",
 119 | 		name: "apiKey",
 120 | 		clientName: "apiKeyClient",
 121 | 		path: `better-auth/plugins`,
 122 | 		clientPath: "better-auth/client/plugins",
 123 | 	},
 124 | 	{
 125 | 		id: "admin",
 126 | 		name: "admin",
 127 | 		clientName: "adminClient",
 128 | 		path: `better-auth/plugins`,
 129 | 		clientPath: "better-auth/client/plugins",
 130 | 	},
 131 | 	{
 132 | 		id: "organization",
 133 | 		name: "organization",
 134 | 		clientName: "organizationClient",
 135 | 		path: `better-auth/plugins`,
 136 | 		clientPath: "better-auth/client/plugins",
 137 | 	},
 138 | 	{
 139 | 		id: "oidc",
 140 | 		name: "oidcProvider",
 141 | 		clientName: "oidcClient",
 142 | 		path: `better-auth/plugins`,
 143 | 		clientPath: "better-auth/client/plugins",
 144 | 	},
 145 | 	{
 146 | 		id: "sso",
 147 | 		name: "sso",
 148 | 		clientName: "ssoClient",
 149 | 		path: `@better-auth/sso`,
 150 | 		clientPath: "@better-auth/sso/client",
 151 | 	},
 152 | 	{
 153 | 		id: "bearer",
 154 | 		name: "bearer",
 155 | 		clientName: undefined,
 156 | 		path: `better-auth/plugins`,
 157 | 		clientPath: undefined,
 158 | 	},
 159 | 	{
 160 | 		id: "multi-session",
 161 | 		name: "multiSession",
 162 | 		clientName: "multiSessionClient",
 163 | 		path: `better-auth/plugins`,
 164 | 		clientPath: "better-auth/client/plugins",
 165 | 	},
 166 | 	{
 167 | 		id: "oauth-proxy",
 168 | 		name: "oAuthProxy",
 169 | 		clientName: undefined,
 170 | 		path: `better-auth/plugins`,
 171 | 		clientPath: undefined,
 172 | 	},
 173 | 	{
 174 | 		id: "open-api",
 175 | 		name: "openAPI",
 176 | 		clientName: undefined,
 177 | 		path: `better-auth/plugins`,
 178 | 		clientPath: undefined,
 179 | 	},
 180 | 	{
 181 | 		id: "jwt",
 182 | 		name: "jwt",
 183 | 		clientName: undefined,
 184 | 		clientPath: undefined,
 185 | 		path: `better-auth/plugins`,
 186 | 	},
 187 | 	{
 188 | 		id: "next-cookies",
 189 | 		name: "nextCookies",
 190 | 		clientPath: undefined,
 191 | 		clientName: undefined,
 192 | 		path: `better-auth/next-js`,
 193 | 	},
 194 | ] as const;
 195 | 
 196 | export type SupportedPlugin = (typeof supportedPlugins)[number];
 197 | 
 198 | const defaultFormatOptions = {
 199 | 	trailingComma: "all" as const,
 200 | 	useTabs: false,
 201 | 	tabWidth: 4,
 202 | };
 203 | 
 204 | const getDefaultAuthConfig = async ({ appName }: { appName?: string }) =>
 205 | 	await prettierFormat(
 206 | 		[
 207 | 			"import { betterAuth } from 'better-auth';",
 208 | 			"",
 209 | 			"export const auth = betterAuth({",
 210 | 			appName ? `appName: "${appName}",` : "",
 211 | 			"plugins: [],",
 212 | 			"});",
 213 | 		].join("\n"),
 214 | 		{
 215 | 			filepath: "auth.ts",
 216 | 			...defaultFormatOptions,
 217 | 		},
 218 | 	);
 219 | 
 220 | type SupportedFrameworks =
 221 | 	| "vanilla"
 222 | 	| "react"
 223 | 	| "vue"
 224 | 	| "svelte"
 225 | 	| "solid"
 226 | 	| "nextjs";
 227 | 
 228 | type Import = {
 229 | 	path: string;
 230 | 	variables:
 231 | 		| { asType?: boolean; name: string; as?: string }[]
 232 | 		| { asType?: boolean; name: string; as?: string };
 233 | };
 234 | 
 235 | const getDefaultAuthClientConfig = async ({
 236 | 	auth_config_path,
 237 | 	framework,
 238 | 	clientPlugins,
 239 | }: {
 240 | 	framework: SupportedFrameworks;
 241 | 	auth_config_path: string;
 242 | 	clientPlugins: {
 243 | 		id: string;
 244 | 		name: string;
 245 | 		contents: string;
 246 | 		imports: Import[];
 247 | 	}[];
 248 | }) => {
 249 | 	function groupImportVariables(): Import[] {
 250 | 		const result: Import[] = [
 251 | 			{
 252 | 				path: "better-auth/client/plugins",
 253 | 				variables: [{ name: "inferAdditionalFields" }],
 254 | 			},
 255 | 		];
 256 | 		for (const plugin of clientPlugins) {
 257 | 			for (const import_ of plugin.imports) {
 258 | 				if (Array.isArray(import_.variables)) {
 259 | 					for (const variable of import_.variables) {
 260 | 						const existingIndex = result.findIndex(
 261 | 							(x) => x.path === import_.path,
 262 | 						);
 263 | 						if (existingIndex !== -1) {
 264 | 							const vars = result[existingIndex]!.variables;
 265 | 							if (Array.isArray(vars)) {
 266 | 								vars.push(variable);
 267 | 							} else {
 268 | 								result[existingIndex]!.variables = [vars, variable];
 269 | 							}
 270 | 						} else {
 271 | 							result.push({
 272 | 								path: import_.path,
 273 | 								variables: [variable],
 274 | 							});
 275 | 						}
 276 | 					}
 277 | 				} else {
 278 | 					const existingIndex = result.findIndex(
 279 | 						(x) => x.path === import_.path,
 280 | 					);
 281 | 					if (existingIndex !== -1) {
 282 | 						const vars = result[existingIndex]!.variables;
 283 | 						if (Array.isArray(vars)) {
 284 | 							vars.push(import_.variables);
 285 | 						} else {
 286 | 							result[existingIndex]!.variables = [vars, import_.variables];
 287 | 						}
 288 | 					} else {
 289 | 						result.push({
 290 | 							path: import_.path,
 291 | 							variables: [import_.variables],
 292 | 						});
 293 | 					}
 294 | 				}
 295 | 			}
 296 | 		}
 297 | 		return result;
 298 | 	}
 299 | 	let imports = groupImportVariables();
 300 | 	let importString = "";
 301 | 	for (const import_ of imports) {
 302 | 		if (Array.isArray(import_.variables)) {
 303 | 			importString += `import { ${import_.variables
 304 | 				.map(
 305 | 					(x) =>
 306 | 						`${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}`,
 307 | 				)
 308 | 				.join(", ")} } from "${import_.path}";\n`;
 309 | 		} else {
 310 | 			importString += `import ${import_.variables.asType ? "type " : ""}${
 311 | 				import_.variables.name
 312 | 			}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${
 313 | 				import_.path
 314 | 			}";\n`;
 315 | 		}
 316 | 	}
 317 | 
 318 | 	return await prettierFormat(
 319 | 		[
 320 | 			`import { createAuthClient } from "better-auth/${
 321 | 				framework === "nextjs"
 322 | 					? "react"
 323 | 					: framework === "vanilla"
 324 | 						? "client"
 325 | 						: framework
 326 | 			}";`,
 327 | 			`import type { auth } from "${auth_config_path}";`,
 328 | 			importString,
 329 | 			``,
 330 | 			`export const authClient = createAuthClient({`,
 331 | 			`baseURL: "http://localhost:3000",`,
 332 | 			`plugins: [inferAdditionalFields<typeof auth>(),${clientPlugins
 333 | 				.map((x) => `${x.name}(${x.contents})`)
 334 | 				.join(", ")}],`,
 335 | 			`});`,
 336 | 		].join("\n"),
 337 | 		{
 338 | 			filepath: "auth-client.ts",
 339 | 			...defaultFormatOptions,
 340 | 		},
 341 | 	);
 342 | };
 343 | 
 344 | const optionsSchema = z.object({
 345 | 	cwd: z.string(),
 346 | 	config: z.string().optional(),
 347 | 	database: z.enum(supportedDatabases).optional(),
 348 | 	"skip-db": z.boolean().optional(),
 349 | 	"skip-plugins": z.boolean().optional(),
 350 | 	"package-manager": z.string().optional(),
 351 | 	tsconfig: z.string().optional(),
 352 | });
 353 | 
 354 | const outroText = `🥳 All Done, Happy Hacking!`;
 355 | 
 356 | export async function initAction(opts: any) {
 357 | 	console.log();
 358 | 	intro("👋 Initializing Better Auth");
 359 | 
 360 | 	const options = optionsSchema.parse(opts);
 361 | 
 362 | 	const cwd = path.resolve(options.cwd);
 363 | 	let packageManagerPreference: "bun" | "pnpm" | "yarn" | "npm" | undefined =
 364 | 		undefined;
 365 | 
 366 | 	let config_path: string = "";
 367 | 	let framework: SupportedFrameworks = "vanilla";
 368 | 
 369 | 	const format = async (code: string) =>
 370 | 		await prettierFormat(code, {
 371 | 			filepath: config_path,
 372 | 			...defaultFormatOptions,
 373 | 		});
 374 | 
 375 | 	// ===== package.json =====
 376 | 	let packageInfo: Record<string, any>;
 377 | 	try {
 378 | 		packageInfo = getPackageInfo(cwd);
 379 | 	} catch (error) {
 380 | 		log.error(`❌ Couldn't read your package.json file. (dir: ${cwd})`);
 381 | 		log.error(JSON.stringify(error, null, 2));
 382 | 		process.exit(1);
 383 | 	}
 384 | 
 385 | 	// ===== ENV files =====
 386 | 	const envFiles = await getEnvFiles(cwd);
 387 | 	if (!envFiles.length) {
 388 | 		outro("❌ No .env files found. Please create an env file first.");
 389 | 		process.exit(0);
 390 | 	}
 391 | 	let targetEnvFile: string;
 392 | 	if (envFiles.includes(".env")) targetEnvFile = ".env";
 393 | 	else if (envFiles.includes(".env.local")) targetEnvFile = ".env.local";
 394 | 	else if (envFiles.includes(".env.development"))
 395 | 		targetEnvFile = ".env.development";
 396 | 	else if (envFiles.length === 1) targetEnvFile = envFiles[0]!;
 397 | 	else targetEnvFile = "none";
 398 | 
 399 | 	// ===== tsconfig.json =====
 400 | 	let tsconfigInfo: Record<string, any>;
 401 | 	try {
 402 | 		const tsconfigPath =
 403 | 			options.tsconfig !== undefined
 404 | 				? path.resolve(cwd, options.tsconfig)
 405 | 				: path.join(cwd, "tsconfig.json");
 406 | 
 407 | 		tsconfigInfo = await getTsconfigInfo(cwd, tsconfigPath);
 408 | 	} catch (error) {
 409 | 		log.error(`❌ Couldn't read your tsconfig.json file. (dir: ${cwd})`);
 410 | 		console.error(error);
 411 | 		process.exit(1);
 412 | 	}
 413 | 	if (
 414 | 		!(
 415 | 			"compilerOptions" in tsconfigInfo &&
 416 | 			"strict" in tsconfigInfo.compilerOptions &&
 417 | 			tsconfigInfo.compilerOptions.strict === true
 418 | 		)
 419 | 	) {
 420 | 		log.warn(
 421 | 			`Better Auth requires your tsconfig.json to have "compilerOptions.strict" set to true.`,
 422 | 		);
 423 | 		const shouldAdd = await confirm({
 424 | 			message: `Would you like us to set ${chalk.bold(
 425 | 				`strict`,
 426 | 			)} to ${chalk.bold(`true`)}?`,
 427 | 		});
 428 | 		if (isCancel(shouldAdd)) {
 429 | 			cancel(`✋ Operation cancelled.`);
 430 | 			process.exit(0);
 431 | 		}
 432 | 		if (shouldAdd) {
 433 | 			try {
 434 | 				await fs.writeFile(
 435 | 					path.join(cwd, "tsconfig.json"),
 436 | 					await prettierFormat(
 437 | 						JSON.stringify(
 438 | 							Object.assign(tsconfigInfo, {
 439 | 								compilerOptions: {
 440 | 									strict: true,
 441 | 								},
 442 | 							}),
 443 | 						),
 444 | 						{ filepath: "tsconfig.json", ...defaultFormatOptions },
 445 | 					),
 446 | 					"utf-8",
 447 | 				);
 448 | 				log.success(`🚀 tsconfig.json successfully updated!`);
 449 | 			} catch (error) {
 450 | 				log.error(
 451 | 					`Failed to add "compilerOptions.strict" to your tsconfig.json file.`,
 452 | 				);
 453 | 				console.error(error);
 454 | 				process.exit(1);
 455 | 			}
 456 | 		}
 457 | 	}
 458 | 
 459 | 	// ===== install better-auth =====
 460 | 	const s = spinner({ indicator: "dots" });
 461 | 	s.start(`Checking better-auth installation`);
 462 | 
 463 | 	let latest_betterauth_version: string;
 464 | 	try {
 465 | 		latest_betterauth_version = await getLatestNpmVersion("better-auth");
 466 | 	} catch (error) {
 467 | 		log.error(`❌ Couldn't get latest version of better-auth.`);
 468 | 		console.error(error);
 469 | 		process.exit(1);
 470 | 	}
 471 | 
 472 | 	if (
 473 | 		!packageInfo.dependencies ||
 474 | 		!Object.keys(packageInfo.dependencies).includes("better-auth")
 475 | 	) {
 476 | 		s.stop("Finished fetching latest version of better-auth.");
 477 | 		const s2 = spinner({ indicator: "dots" });
 478 | 		const shouldInstallBetterAuthDep = await confirm({
 479 | 			message: `Would you like to install Better Auth?`,
 480 | 		});
 481 | 		if (isCancel(shouldInstallBetterAuthDep)) {
 482 | 			cancel(`✋ Operation cancelled.`);
 483 | 			process.exit(0);
 484 | 		}
 485 | 		if (packageManagerPreference === undefined) {
 486 | 			packageManagerPreference = await getPackageManager();
 487 | 		}
 488 | 		if (shouldInstallBetterAuthDep) {
 489 | 			s2.start(
 490 | 				`Installing Better Auth using ${chalk.bold(packageManagerPreference)}`,
 491 | 			);
 492 | 			try {
 493 | 				const start = Date.now();
 494 | 				await installDependencies({
 495 | 					dependencies: ["better-auth@latest"],
 496 | 					packageManager: packageManagerPreference,
 497 | 					cwd: cwd,
 498 | 				});
 499 | 				s2.stop(
 500 | 					`Better Auth installed ${chalk.greenBright(
 501 | 						`successfully`,
 502 | 					)}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`,
 503 | 				);
 504 | 			} catch (error: any) {
 505 | 				s2.stop(`Failed to install Better Auth:`);
 506 | 				console.error(error);
 507 | 				process.exit(1);
 508 | 			}
 509 | 		}
 510 | 	} else if (
 511 | 		packageInfo.dependencies["better-auth"] !== "workspace:*" &&
 512 | 		semver.lt(
 513 | 			semver.coerce(packageInfo.dependencies["better-auth"])?.toString()!,
 514 | 			semver.clean(latest_betterauth_version)!,
 515 | 		)
 516 | 	) {
 517 | 		s.stop("Finished fetching latest version of better-auth.");
 518 | 		const shouldInstallBetterAuthDep = await confirm({
 519 | 			message: `Your current Better Auth dependency is out-of-date. Would you like to update it? (${chalk.bold(
 520 | 				packageInfo.dependencies["better-auth"],
 521 | 			)} → ${chalk.bold(`v${latest_betterauth_version}`)})`,
 522 | 		});
 523 | 		if (isCancel(shouldInstallBetterAuthDep)) {
 524 | 			cancel(`✋ Operation cancelled.`);
 525 | 			process.exit(0);
 526 | 		}
 527 | 		if (shouldInstallBetterAuthDep) {
 528 | 			if (packageManagerPreference === undefined) {
 529 | 				packageManagerPreference = await getPackageManager();
 530 | 			}
 531 | 			const s = spinner({ indicator: "dots" });
 532 | 			s.start(
 533 | 				`Updating Better Auth using ${chalk.bold(packageManagerPreference)}`,
 534 | 			);
 535 | 			try {
 536 | 				const start = Date.now();
 537 | 				await installDependencies({
 538 | 					dependencies: ["better-auth@latest"],
 539 | 					packageManager: packageManagerPreference,
 540 | 					cwd: cwd,
 541 | 				});
 542 | 				s.stop(
 543 | 					`Better Auth updated ${chalk.greenBright(
 544 | 						`successfully`,
 545 | 					)}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`,
 546 | 				);
 547 | 			} catch (error: any) {
 548 | 				s.stop(`Failed to update Better Auth:`);
 549 | 				log.error(error.message);
 550 | 				process.exit(1);
 551 | 			}
 552 | 		}
 553 | 	} else {
 554 | 		s.stop(`Better Auth dependencies are ${chalk.greenBright(`up to date`)}!`);
 555 | 	}
 556 | 
 557 | 	// ===== appName =====
 558 | 
 559 | 	const packageJson = getPackageInfo(cwd);
 560 | 	let appName: string;
 561 | 	if (!packageJson.name) {
 562 | 		const newAppName = await text({
 563 | 			message: "What is the name of your application?",
 564 | 		});
 565 | 		if (isCancel(newAppName)) {
 566 | 			cancel("✋ Operation cancelled.");
 567 | 			process.exit(0);
 568 | 		}
 569 | 		appName = newAppName;
 570 | 	} else {
 571 | 		appName = packageJson.name;
 572 | 	}
 573 | 
 574 | 	// ===== config path =====
 575 | 
 576 | 	let possiblePaths = ["auth.ts", "auth.tsx", "auth.js", "auth.jsx"];
 577 | 	possiblePaths = [
 578 | 		...possiblePaths,
 579 | 		...possiblePaths.map((it) => `lib/server/${it}`),
 580 | 		...possiblePaths.map((it) => `server/${it}`),
 581 | 		...possiblePaths.map((it) => `lib/${it}`),
 582 | 		...possiblePaths.map((it) => `utils/${it}`),
 583 | 	];
 584 | 	possiblePaths = [
 585 | 		...possiblePaths,
 586 | 		...possiblePaths.map((it) => `src/${it}`),
 587 | 		...possiblePaths.map((it) => `app/${it}`),
 588 | 	];
 589 | 
 590 | 	if (options.config) {
 591 | 		config_path = path.join(cwd, options.config);
 592 | 	} else {
 593 | 		for (const possiblePath of possiblePaths) {
 594 | 			const doesExist = existsSync(path.join(cwd, possiblePath));
 595 | 			if (doesExist) {
 596 | 				config_path = path.join(cwd, possiblePath);
 597 | 				break;
 598 | 			}
 599 | 		}
 600 | 	}
 601 | 
 602 | 	// ===== create auth config =====
 603 | 	let current_user_config = "";
 604 | 	let database: SupportedDatabases | null = null;
 605 | 	let add_plugins: SupportedPlugin[] = [];
 606 | 
 607 | 	if (!config_path) {
 608 | 		const shouldCreateAuthConfig = await select({
 609 | 			message: `Would you like to create an auth config file?`,
 610 | 			options: [
 611 | 				{ label: "Yes", value: "yes" },
 612 | 				{ label: "No", value: "no" },
 613 | 			],
 614 | 		});
 615 | 		if (isCancel(shouldCreateAuthConfig)) {
 616 | 			cancel(`✋ Operation cancelled.`);
 617 | 			process.exit(0);
 618 | 		}
 619 | 		if (shouldCreateAuthConfig === "yes") {
 620 | 			const shouldSetupDb = await confirm({
 621 | 				message: `Would you like to set up your ${chalk.bold(`database`)}?`,
 622 | 				initialValue: true,
 623 | 			});
 624 | 			if (isCancel(shouldSetupDb)) {
 625 | 				cancel(`✋ Operating cancelled.`);
 626 | 				process.exit(0);
 627 | 			}
 628 | 			if (shouldSetupDb) {
 629 | 				const prompted_database = await select({
 630 | 					message: "Choose a Database Dialect",
 631 | 					options: supportedDatabases.map((it) => ({ value: it, label: it })),
 632 | 				});
 633 | 				if (isCancel(prompted_database)) {
 634 | 					cancel(`✋ Operating cancelled.`);
 635 | 					process.exit(0);
 636 | 				}
 637 | 				database = prompted_database;
 638 | 			}
 639 | 
 640 | 			if (options["skip-plugins"] !== false) {
 641 | 				const shouldSetupPlugins = await confirm({
 642 | 					message: `Would you like to set up ${chalk.bold(`plugins`)}?`,
 643 | 				});
 644 | 				if (isCancel(shouldSetupPlugins)) {
 645 | 					cancel(`✋ Operating cancelled.`);
 646 | 					process.exit(0);
 647 | 				}
 648 | 				if (shouldSetupPlugins) {
 649 | 					const prompted_plugins = await multiselect({
 650 | 						message: "Select your new plugins",
 651 | 						options: supportedPlugins
 652 | 							.filter((x) => x.id !== "next-cookies")
 653 | 							.map((x) => ({ value: x.id, label: x.id })),
 654 | 						required: false,
 655 | 					});
 656 | 					if (isCancel(prompted_plugins)) {
 657 | 						cancel(`✋ Operating cancelled.`);
 658 | 						process.exit(0);
 659 | 					}
 660 | 					add_plugins = prompted_plugins.map(
 661 | 						(x) => supportedPlugins.find((y) => y.id === x)!,
 662 | 					);
 663 | 
 664 | 					const possible_next_config_paths = [
 665 | 						"next.config.js",
 666 | 						"next.config.ts",
 667 | 						"next.config.mjs",
 668 | 						".next/server/next.config.js",
 669 | 						".next/server/next.config.ts",
 670 | 						".next/server/next.config.mjs",
 671 | 					];
 672 | 					for (const possible_next_config_path of possible_next_config_paths) {
 673 | 						if (existsSync(path.join(cwd, possible_next_config_path))) {
 674 | 							framework = "nextjs";
 675 | 							break;
 676 | 						}
 677 | 					}
 678 | 					if (framework === "nextjs") {
 679 | 						const result = await confirm({
 680 | 							message: `It looks like you're using NextJS. Do you want to add the next-cookies plugin? ${chalk.bold(
 681 | 								`(Recommended)`,
 682 | 							)}`,
 683 | 						});
 684 | 						if (isCancel(result)) {
 685 | 							cancel(`✋ Operating cancelled.`);
 686 | 							process.exit(0);
 687 | 						}
 688 | 						if (result) {
 689 | 							add_plugins.push(
 690 | 								supportedPlugins.find((x) => x.id === "next-cookies")!,
 691 | 							);
 692 | 						}
 693 | 					}
 694 | 				}
 695 | 			}
 696 | 
 697 | 			const filePath = path.join(cwd, "auth.ts");
 698 | 			config_path = filePath;
 699 | 			log.info(`Creating auth config file: ${filePath}`);
 700 | 			try {
 701 | 				current_user_config = await getDefaultAuthConfig({
 702 | 					appName,
 703 | 				});
 704 | 				const { dependencies, envs, generatedCode } = await generateAuthConfig({
 705 | 					current_user_config,
 706 | 					format,
 707 | 					//@ts-expect-error
 708 | 					s,
 709 | 					plugins: add_plugins,
 710 | 					database,
 711 | 				});
 712 | 				current_user_config = generatedCode;
 713 | 				await fs.writeFile(filePath, current_user_config);
 714 | 				config_path = filePath;
 715 | 				log.success(`🚀 Auth config file successfully created!`);
 716 | 
 717 | 				if (envs.length !== 0) {
 718 | 					log.info(
 719 | 						`There are ${envs.length} environment variables for your database of choice.`,
 720 | 					);
 721 | 					const shouldUpdateEnvs = await confirm({
 722 | 						message: `Would you like us to update your ENV files?`,
 723 | 					});
 724 | 					if (isCancel(shouldUpdateEnvs)) {
 725 | 						cancel("✋ Operation cancelled.");
 726 | 						process.exit(0);
 727 | 					}
 728 | 					if (shouldUpdateEnvs) {
 729 | 						const filesToUpdate = await multiselect({
 730 | 							message: "Select the .env files you want to update",
 731 | 							options: envFiles.map((x) => ({
 732 | 								value: path.join(cwd, x),
 733 | 								label: x,
 734 | 							})),
 735 | 							required: false,
 736 | 						});
 737 | 						if (isCancel(filesToUpdate)) {
 738 | 							cancel("✋ Operation cancelled.");
 739 | 							process.exit(0);
 740 | 						}
 741 | 						if (filesToUpdate.length === 0) {
 742 | 							log.info("No .env files to update. Skipping...");
 743 | 						} else {
 744 | 							try {
 745 | 								await updateEnvs({
 746 | 									files: filesToUpdate,
 747 | 									envs,
 748 | 									isCommented: true,
 749 | 								});
 750 | 							} catch (error) {
 751 | 								log.error(`Failed to update .env files:`);
 752 | 								log.error(JSON.stringify(error, null, 2));
 753 | 								process.exit(1);
 754 | 							}
 755 | 							log.success(`🚀 ENV files successfully updated!`);
 756 | 						}
 757 | 					}
 758 | 				}
 759 | 				if (dependencies.length !== 0) {
 760 | 					log.info(
 761 | 						`There are ${
 762 | 							dependencies.length
 763 | 						} dependencies to install. (${dependencies
 764 | 							.map((x) => chalk.green(x))
 765 | 							.join(", ")})`,
 766 | 					);
 767 | 					const shouldInstallDeps = await confirm({
 768 | 						message: `Would you like us to install dependencies?`,
 769 | 					});
 770 | 					if (isCancel(shouldInstallDeps)) {
 771 | 						cancel("✋ Operation cancelled.");
 772 | 						process.exit(0);
 773 | 					}
 774 | 					if (shouldInstallDeps) {
 775 | 						const s = spinner({ indicator: "dots" });
 776 | 						if (packageManagerPreference === undefined) {
 777 | 							packageManagerPreference = await getPackageManager();
 778 | 						}
 779 | 						s.start(
 780 | 							`Installing dependencies using ${chalk.bold(
 781 | 								packageManagerPreference,
 782 | 							)}...`,
 783 | 						);
 784 | 						try {
 785 | 							const start = Date.now();
 786 | 							await installDependencies({
 787 | 								dependencies: dependencies,
 788 | 								packageManager: packageManagerPreference,
 789 | 								cwd: cwd,
 790 | 							});
 791 | 							s.stop(
 792 | 								`Dependencies installed ${chalk.greenBright(
 793 | 									`successfully`,
 794 | 								)} ${chalk.gray(
 795 | 									`(${formatMilliseconds(Date.now() - start)})`,
 796 | 								)}`,
 797 | 							);
 798 | 						} catch (error: any) {
 799 | 							s.stop(
 800 | 								`Failed to install dependencies using ${packageManagerPreference}:`,
 801 | 							);
 802 | 							log.error(error.message);
 803 | 							process.exit(1);
 804 | 						}
 805 | 					}
 806 | 				}
 807 | 			} catch (error) {
 808 | 				log.error(`Failed to create auth config file: ${filePath}`);
 809 | 				console.error(error);
 810 | 				process.exit(1);
 811 | 			}
 812 | 		} else if (shouldCreateAuthConfig === "no") {
 813 | 			log.info(`Skipping auth config file creation.`);
 814 | 		}
 815 | 	} else {
 816 | 		log.message();
 817 | 		log.success(`Found auth config file. ${chalk.gray(`(${config_path})`)}`);
 818 | 		log.message();
 819 | 	}
 820 | 
 821 | 	// ===== auth client path =====
 822 | 
 823 | 	let possibleClientPaths = [
 824 | 		"auth-client.ts",
 825 | 		"auth-client.tsx",
 826 | 		"auth-client.js",
 827 | 		"auth-client.jsx",
 828 | 		"client.ts",
 829 | 		"client.tsx",
 830 | 		"client.js",
 831 | 		"client.jsx",
 832 | 	];
 833 | 	possibleClientPaths = [
 834 | 		...possibleClientPaths,
 835 | 		...possibleClientPaths.map((it) => `lib/server/${it}`),
 836 | 		...possibleClientPaths.map((it) => `server/${it}`),
 837 | 		...possibleClientPaths.map((it) => `lib/${it}`),
 838 | 		...possibleClientPaths.map((it) => `utils/${it}`),
 839 | 	];
 840 | 	possibleClientPaths = [
 841 | 		...possibleClientPaths,
 842 | 		...possibleClientPaths.map((it) => `src/${it}`),
 843 | 		...possibleClientPaths.map((it) => `app/${it}`),
 844 | 	];
 845 | 
 846 | 	let authClientConfigPath: string | null = null;
 847 | 	for (const possiblePath of possibleClientPaths) {
 848 | 		const doesExist = existsSync(path.join(cwd, possiblePath));
 849 | 		if (doesExist) {
 850 | 			authClientConfigPath = path.join(cwd, possiblePath);
 851 | 			break;
 852 | 		}
 853 | 	}
 854 | 
 855 | 	if (!authClientConfigPath) {
 856 | 		const choice = await select({
 857 | 			message: `Would you like to create an auth client config file?`,
 858 | 			options: [
 859 | 				{ label: "Yes", value: "yes" },
 860 | 				{ label: "No", value: "no" },
 861 | 			],
 862 | 		});
 863 | 		if (isCancel(choice)) {
 864 | 			cancel(`✋ Operation cancelled.`);
 865 | 			process.exit(0);
 866 | 		}
 867 | 		if (choice === "yes") {
 868 | 			authClientConfigPath = path.join(cwd, "auth-client.ts");
 869 | 			log.info(`Creating auth client config file: ${authClientConfigPath}`);
 870 | 			try {
 871 | 				let contents = await getDefaultAuthClientConfig({
 872 | 					auth_config_path: (
 873 | 						"./" + path.join(config_path.replace(cwd, ""))
 874 | 					).replace(".//", "./"),
 875 | 					clientPlugins: add_plugins
 876 | 						.filter((x) => x.clientName)
 877 | 						.map((plugin) => {
 878 | 							let contents = "";
 879 | 							if (plugin.id === "one-tap") {
 880 | 								contents = `{ clientId: "MY_CLIENT_ID" }`;
 881 | 							}
 882 | 							return {
 883 | 								contents,
 884 | 								id: plugin.id,
 885 | 								name: plugin.clientName!,
 886 | 								imports: [
 887 | 									{
 888 | 										path: "better-auth/client/plugins",
 889 | 										variables: [{ name: plugin.clientName! }],
 890 | 									},
 891 | 								],
 892 | 							};
 893 | 						}),
 894 | 					framework: framework,
 895 | 				});
 896 | 				await fs.writeFile(authClientConfigPath, contents);
 897 | 				log.success(`🚀 Auth client config file successfully created!`);
 898 | 			} catch (error) {
 899 | 				log.error(
 900 | 					`Failed to create auth client config file: ${authClientConfigPath}`,
 901 | 				);
 902 | 				log.error(JSON.stringify(error, null, 2));
 903 | 				process.exit(1);
 904 | 			}
 905 | 		} else if (choice === "no") {
 906 | 			log.info(`Skipping auth client config file creation.`);
 907 | 		}
 908 | 	} else {
 909 | 		log.success(
 910 | 			`Found auth client config file. ${chalk.gray(
 911 | 				`(${authClientConfigPath})`,
 912 | 			)}`,
 913 | 		);
 914 | 	}
 915 | 
 916 | 	if (targetEnvFile !== "none") {
 917 | 		try {
 918 | 			const fileContents = await fs.readFile(
 919 | 				path.join(cwd, targetEnvFile),
 920 | 				"utf8",
 921 | 			);
 922 | 			const parsed = parse(fileContents);
 923 | 			let isMissingSecret = false;
 924 | 			let isMissingUrl = false;
 925 | 			if (parsed.BETTER_AUTH_SECRET === undefined) isMissingSecret = true;
 926 | 			if (parsed.BETTER_AUTH_URL === undefined) isMissingUrl = true;
 927 | 			if (isMissingSecret || isMissingUrl) {
 928 | 				let txt = "";
 929 | 				if (isMissingSecret && !isMissingUrl)
 930 | 					txt = chalk.bold(`BETTER_AUTH_SECRET`);
 931 | 				else if (!isMissingSecret && isMissingUrl)
 932 | 					txt = chalk.bold(`BETTER_AUTH_URL`);
 933 | 				else
 934 | 					txt =
 935 | 						chalk.bold.underline(`BETTER_AUTH_SECRET`) +
 936 | 						` and ` +
 937 | 						chalk.bold.underline(`BETTER_AUTH_URL`);
 938 | 				log.warn(`Missing ${txt} in ${targetEnvFile}`);
 939 | 
 940 | 				const shouldAdd = await select({
 941 | 					message: `Do you want to add ${txt} to ${targetEnvFile}?`,
 942 | 					options: [
 943 | 						{ label: "Yes", value: "yes" },
 944 | 						{ label: "No", value: "no" },
 945 | 						{ label: "Choose other file(s)", value: "other" },
 946 | 					],
 947 | 				});
 948 | 				if (isCancel(shouldAdd)) {
 949 | 					cancel(`✋ Operation cancelled.`);
 950 | 					process.exit(0);
 951 | 				}
 952 | 				let envs: string[] = [];
 953 | 				if (isMissingSecret) {
 954 | 					envs.push("BETTER_AUTH_SECRET");
 955 | 				}
 956 | 				if (isMissingUrl) {
 957 | 					envs.push("BETTER_AUTH_URL");
 958 | 				}
 959 | 				if (shouldAdd === "yes") {
 960 | 					try {
 961 | 						await updateEnvs({
 962 | 							files: [path.join(cwd, targetEnvFile)],
 963 | 							envs: envs,
 964 | 							isCommented: false,
 965 | 						});
 966 | 					} catch (error) {
 967 | 						log.error(`Failed to add ENV variables to ${targetEnvFile}`);
 968 | 						log.error(JSON.stringify(error, null, 2));
 969 | 						process.exit(1);
 970 | 					}
 971 | 					log.success(`🚀 ENV variables successfully added!`);
 972 | 					if (isMissingUrl) {
 973 | 						log.info(
 974 | 							`Be sure to update your BETTER_AUTH_URL according to your app's needs.`,
 975 | 						);
 976 | 					}
 977 | 				} else if (shouldAdd === "no") {
 978 | 					log.info(`Skipping ENV step.`);
 979 | 				} else if (shouldAdd === "other") {
 980 | 					if (!envFiles.length) {
 981 | 						cancel("No env files found. Please create an env file first.");
 982 | 						process.exit(0);
 983 | 					}
 984 | 					const envFilesToUpdate = await multiselect({
 985 | 						message: "Select the .env files you want to update",
 986 | 						options: envFiles.map((x) => ({
 987 | 							value: path.join(cwd, x),
 988 | 							label: x,
 989 | 						})),
 990 | 						required: false,
 991 | 					});
 992 | 					if (isCancel(envFilesToUpdate)) {
 993 | 						cancel("✋ Operation cancelled.");
 994 | 						process.exit(0);
 995 | 					}
 996 | 					if (envFilesToUpdate.length === 0) {
 997 | 						log.info("No .env files to update. Skipping...");
 998 | 					} else {
 999 | 						try {
1000 | 							await updateEnvs({
1001 | 								files: envFilesToUpdate,
1002 | 								envs: envs,
1003 | 								isCommented: false,
1004 | 							});
1005 | 						} catch (error) {
1006 | 							log.error(`Failed to update .env files:`);
1007 | 							log.error(JSON.stringify(error, null, 2));
1008 | 							process.exit(1);
1009 | 						}
1010 | 						log.success(`🚀 ENV files successfully updated!`);
1011 | 					}
1012 | 				}
1013 | 			}
1014 | 		} catch (error) {
1015 | 			// if fails, ignore, and do not proceed with ENV operations.
1016 | 		}
1017 | 	}
1018 | 
1019 | 	outro(outroText);
1020 | 	console.log();
1021 | 	process.exit(0);
1022 | }
1023 | 
1024 | // ===== Init Command =====
1025 | 
1026 | export const init = new Command("init")
1027 | 	.option("-c, --cwd <cwd>", "The working directory.", process.cwd())
1028 | 	.option(
1029 | 		"--config <config>",
1030 | 		"The path to the auth configuration file. defaults to the first `auth.ts` file found.",
1031 | 	)
1032 | 	.option("--tsconfig <tsconfig>", "The path to the tsconfig file.")
1033 | 	.option("--skip-db", "Skip the database setup.")
1034 | 	.option("--skip-plugins", "Skip the plugins setup.")
1035 | 	.option(
1036 | 		"--package-manager <package-manager>",
1037 | 		"The package manager you want to use.",
1038 | 	)
1039 | 	.action(initAction);
1040 | 
1041 | async function getLatestNpmVersion(packageName: string): Promise<string> {
1042 | 	try {
1043 | 		const response = await fetch(`https://registry.npmjs.org/${packageName}`);
1044 | 
1045 | 		if (!response.ok) {
1046 | 			throw new Error(`Package not found: ${response.statusText}`);
1047 | 		}
1048 | 
1049 | 		const data = await response.json();
1050 | 		return data["dist-tags"].latest; // Get the latest version from dist-tags
1051 | 	} catch (error: any) {
1052 | 		throw error?.message;
1053 | 	}
1054 | }
1055 | 
1056 | async function getPackageManager() {
1057 | 	const { hasBun, hasPnpm } = await checkPackageManagers();
1058 | 	if (!hasBun && !hasPnpm) return "npm";
1059 | 
1060 | 	const packageManagerOptions: {
1061 | 		value: "bun" | "pnpm" | "yarn" | "npm";
1062 | 		label?: string;
1063 | 		hint?: string;
1064 | 	}[] = [];
1065 | 
1066 | 	if (hasPnpm) {
1067 | 		packageManagerOptions.push({
1068 | 			value: "pnpm",
1069 | 			label: "pnpm",
1070 | 			hint: "recommended",
1071 | 		});
1072 | 	}
1073 | 	if (hasBun) {
1074 | 		packageManagerOptions.push({
1075 | 			value: "bun",
1076 | 			label: "bun",
1077 | 		});
1078 | 	}
1079 | 	packageManagerOptions.push({
1080 | 		value: "npm",
1081 | 		hint: "not recommended",
1082 | 	});
1083 | 
1084 | 	let packageManager = await select({
1085 | 		message: "Choose a package manager",
1086 | 		options: packageManagerOptions,
1087 | 	});
1088 | 	if (isCancel(packageManager)) {
1089 | 		cancel(`Operation cancelled.`);
1090 | 		process.exit(0);
1091 | 	}
1092 | 	return packageManager;
1093 | }
1094 | 
1095 | async function getEnvFiles(cwd: string) {
1096 | 	const files = await fs.readdir(cwd);
1097 | 	return files.filter((x) => x.startsWith(".env"));
1098 | }
1099 | 
1100 | async function updateEnvs({
1101 | 	envs,
1102 | 	files,
1103 | 	isCommented,
1104 | }: {
1105 | 	/**
1106 | 	 * The ENVs to append to the file
1107 | 	 */
1108 | 	envs: string[];
1109 | 	/**
1110 | 	 * Full file paths
1111 | 	 */
1112 | 	files: string[];
1113 | 	/**
1114 | 	 * Whether to comment the all of the envs or not
1115 | 	 */
1116 | 	isCommented: boolean;
1117 | }) {
1118 | 	let previouslyGeneratedSecret: string | null = null;
1119 | 	for (const file of files) {
1120 | 		const content = await fs.readFile(file, "utf8");
1121 | 		const lines = content.split("\n");
1122 | 		const newLines = envs.map(
1123 | 			(x) =>
1124 | 				`${isCommented ? "# " : ""}${x}=${
1125 | 					getEnvDescription(x) ?? `"some_value"`
1126 | 				}`,
1127 | 		);
1128 | 		newLines.push("");
1129 | 		newLines.push(...lines);
1130 | 		await fs.writeFile(file, newLines.join("\n"), "utf8");
1131 | 	}
1132 | 
1133 | 	function getEnvDescription(env: string) {
1134 | 		if (env === "DATABASE_HOST") {
1135 | 			return `"The host of your database"`;
1136 | 		}
1137 | 		if (env === "DATABASE_PORT") {
1138 | 			return `"The port of your database"`;
1139 | 		}
1140 | 		if (env === "DATABASE_USER") {
1141 | 			return `"The username of your database"`;
1142 | 		}
1143 | 		if (env === "DATABASE_PASSWORD") {
1144 | 			return `"The password of your database"`;
1145 | 		}
1146 | 		if (env === "DATABASE_NAME") {
1147 | 			return `"The name of your database"`;
1148 | 		}
1149 | 		if (env === "DATABASE_URL") {
1150 | 			return `"The URL of your database"`;
1151 | 		}
1152 | 		if (env === "BETTER_AUTH_SECRET") {
1153 | 			previouslyGeneratedSecret =
1154 | 				previouslyGeneratedSecret ?? generateSecretHash();
1155 | 			return `"${previouslyGeneratedSecret}"`;
1156 | 		}
1157 | 		if (env === "BETTER_AUTH_URL") {
1158 | 			return `"http://localhost:3000" # Your APP URL`;
1159 | 		}
1160 | 	}
1161 | }
1162 | 
```
Page 53/67FirstPrevNextLast