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