This is page 15 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── 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-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── 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 -------------------------------------------------------------------------------- /docs/app/api/og-release/route.tsx: -------------------------------------------------------------------------------- ```typescript import { ImageResponse } from "@vercel/og"; import { z } from "zod"; export const runtime = "edge"; const ogSchema = z.object({ heading: z.string(), description: z.string().optional(), date: z.string().optional(), }); export async function GET(req: Request) { try { const geist = await fetch( new URL("../../../assets/Geist.ttf", import.meta.url), ).then((res) => res.arrayBuffer()); const url = new URL(req.url); const urlParamsValues = Object.fromEntries(url.searchParams); const validParams = ogSchema.parse(urlParamsValues); const { heading, description, date } = validParams; const trueHeading = heading.length > 140 ? `${heading.substring(0, 140)}...` : heading; return new ImageResponse( <div tw="flex w-full h-full relative flex-col" style={{ background: "radial-gradient(circle 230px at 0% 0%, #000000, #000000)", fontFamily: "Geist", color: "white", }} > <div tw="flex w-full h-full relative" style={{ borderRadius: "10px", border: "1px solid rgba(32, 34, 34, 0.5)", }} > <div tw="absolute" style={{ width: "350px", height: "120px", borderRadius: "100px", background: "#c7c7c7", opacity: 0.21, filter: "blur(35px)", transform: "rotate(50deg)", top: "18%", left: "0%", }} /> <div tw="flex flex-col w-full relative h-full p-8" style={{ gap: "14px", position: "relative", zIndex: 999, }} > <div tw="absolute bg-repeat w-full h-full" style={{ width: "100%", height: "100%", zIndex: 999, background: "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnN2Z2pzPSJodHRwOi8vc3ZnanMuZGV2L3N2Z2pzIiB2aWV3Qm94PSIwIDAgODAwIDgwMCIgd2lkdGg9IjgwMCIgaGVpZ2h0PSI4MDAiPjxnIHN0cm9rZS13aWR0aD0iMy41IiBzdHJva2U9ImhzbGEoMCwgMCUsIDEwMCUsIDEuMDApIiBmaWxsPSJub25lIiBvcGFjaXR5PSIwLjUiPjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iNDAwIiB4PSIwIiB5PSIwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjMxNTc4OTQ3MzYiIGN4PSIwIiBjeT0iMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjQwMCIgeT0iMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iNDAwIiBjeT0iMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjgwMCIgeT0iMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iODAwIiBjeT0iMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjAiIHk9IjQwMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iMCIgY3k9IjQwMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjQwMCIgeT0iNDAwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjMxNTc4OTQ3MzYiIGN4PSI0MDAiIGN5PSI0MDAiIGZpbGw9ImhzbGEoMCwgMCUsIDEwMCUsIDEuMDApIiBzdHJva2U9Im5vbmUiPjwvY2lyY2xlPjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iNDAwIiB4PSI4MDAiIHk9IjQwMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iODAwIiBjeT0iNDAwIiBmaWxsPSJoc2xhKDAsIDAlLCAxMDAlLCAxLjAwKSIgc3Ryb2tlPSJub25lIj48L2NpcmNsZT48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeD0iMCIgeT0iODAwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjMxNTc4OTQ3MzYiIGN4PSIwIiBjeT0iODAwIiBmaWxsPSJoc2xhKDAsIDAlLCAxMDAlLCAxLjAwKSIgc3Ryb2tlPSJub25lIj48L2NpcmNsZT48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeD0iNDAwIiB5PSI4MDAiIG9wYWNpdHk9IjAuMTUiPjwvcmVjdD48Y2lyY2xlIHI9IjEwLjg1NTI2MzE1Nzg5NDczNiIgY3g9IjQwMCIgY3k9IjgwMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjgwMCIgeT0iODAwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjM1NTc4OTQ3MzYiIGN4PSI4MDAiIGN5PSI4MDAiIGZpbGw9ImhzbGEoMCwgMCUsIDEwMCUsIDEuMDApIiBzdHJva2U9Im5vbmUiPjwvY2lyY2xlPjwvZz48L3N2Zz4=')", backgroundSize: "25px 25px", display: "flex", alignItems: "flex-start", justifyContent: "flex-start", position: "relative", flexDirection: "column", textAlign: "left", paddingLeft: "170px", gap: "14px", }} /> <div tw="flex text-6xl absolute bottom-56 isolate font-bold" style={{ paddingLeft: "170px", paddingTop: "200px", background: "linear-gradient(45deg, #000000 4%, #fff, #000)", backgroundClip: "text", color: "transparent", }} > {trueHeading} </div> <div tw="flex absolute bottom-44 z-[999] text-2xl" style={{ paddingLeft: "170px", background: "linear-gradient(10deg, #d4d4d8, 04%, #fff, #d4d4d8)", backgroundClip: "text", opacity: 0.7, color: "transparent", }} > {description} </div> <div tw="flex text-2xl absolute bottom-28 z-[999]" style={{ paddingLeft: "170px", background: "linear-gradient(10deg, #d4d4d8, 04%, #fff, #d4d4d8)", backgroundClip: "text", opacity: 0.8, color: "transparent", }} > {date} </div> </div> {/* Lines */} <div tw="absolute top-10% w-full h-px" style={{ background: "linear-gradient(90deg, #888888 30%, #1d1f1f 70%)", }} /> <div tw="absolute bottom-10% w-full h-px" style={{ background: "#2c2c2c", }} /> <div tw="absolute left-10% h-full w-px" style={{ background: "linear-gradient(180deg, #747474 30%, #222424 70%)", }} /> <div tw="absolute right-10% h-full w-px" style={{ background: "#2c2c2c", }} /> </div> </div>, { width: 1200, height: 630, fonts: [ { name: "Geist", data: geist, weight: 400, style: "normal", }, ], }, ); } catch (err) { console.log({ err }); return new Response("Failed to generate the OG image", { status: 500 }); } } ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/apple.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { APIError } from "better-call"; import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { refreshAccessToken, createAuthorizationURL, validateAuthorizationCode, } from "../oauth2"; export interface AppleProfile { /** * The subject registered claim identifies the principal that’s the subject * of the identity token. Because this token is for your app, the value is * the unique identifier for the user. */ sub: string; /** * A String value representing the user's email address. * The email address is either the user's real email address or the proxy * address, depending on their status private email relay service. */ email: string; /** * A string or Boolean value that indicates whether the service verifies * the email. The value can either be a string ("true" or "false") or a * Boolean (true or false). The system may not verify email addresses for * Sign in with Apple at Work & School users, and this claim is "false" or * false for those users. */ email_verified: true | "true"; /** * A string or Boolean value that indicates whether the email that the user * shares is the proxy address. The value can either be a string ("true" or * "false") or a Boolean (true or false). */ is_private_email: boolean; /** * An Integer value that indicates whether the user appears to be a real * person. Use the value of this claim to mitigate fraud. The possible * values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal). For * more information, see ASUserDetectionStatus. This claim is present only * in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14 * and later. The claim isn’t present or supported for web-based apps. */ real_user_status: number; /** * The user’s full name in the format provided during the authorization * process. */ name: string; /** * The URL to the user's profile picture. */ picture: string; user?: AppleNonConformUser; } /** * This is the shape of the `user` query parameter that Apple sends the first * time the user consents to the app. * @see https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server./ */ export interface AppleNonConformUser { name: { firstName: string; lastName: string; }; email: string; } export interface AppleOptions extends ProviderOptions<AppleProfile> { clientId: string; appBundleIdentifier?: string; audience?: string | string[]; } export const apple = (options: AppleOptions) => { const tokenEndpoint = "https://appleid.apple.com/auth/token"; return { id: "apple", name: "Apple", async createAuthorizationURL({ state, scopes, redirectURI }) { const _scope = options.disableDefaultScope ? [] : ["email", "name"]; options.scope && _scope.push(...options.scope); scopes && _scope.push(...scopes); const url = await createAuthorizationURL({ id: "apple", options, authorizationEndpoint: "https://appleid.apple.com/auth/authorize", scopes: _scope, state, redirectURI, responseMode: "form_post", responseType: "code id_token", }); return url; }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { return validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint, }); }, async verifyIdToken(token, nonce) { if (options.disableIdTokenSignIn) { return false; } if (options.verifyIdToken) { return options.verifyIdToken(token, nonce); } const decodedHeader = decodeProtectedHeader(token); const { kid, alg: jwtAlg } = decodedHeader; if (!kid || !jwtAlg) return false; const publicKey = await getApplePublicKey(kid); const { payload: jwtClaims } = await jwtVerify(token, publicKey, { algorithms: [jwtAlg], issuer: "https://appleid.apple.com", audience: options.audience && options.audience.length ? options.audience : options.appBundleIdentifier ? options.appBundleIdentifier : options.clientId, maxTokenAge: "1h", }); ["email_verified", "is_private_email"].forEach((field) => { if (jwtClaims[field] !== undefined) { jwtClaims[field] = Boolean(jwtClaims[field]); } }); if (nonce && jwtClaims.nonce !== nonce) { return false; } return !!jwtClaims; }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint: "https://appleid.apple.com/auth/token", }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } if (!token.idToken) { return null; } const profile = decodeJwt<AppleProfile>(token.idToken); if (!profile) { return null; } const name = token.user ? `${token.user.name?.firstName} ${token.user.name?.lastName}` : profile.name || profile.email; const emailVerified = typeof profile.email_verified === "boolean" ? profile.email_verified : profile.email_verified === "true"; const enrichedProfile = { ...profile, name, }; const userMap = await options.mapProfileToUser?.(enrichedProfile); return { user: { id: profile.sub, name: enrichedProfile.name, emailVerified: emailVerified, email: profile.email, ...userMap, }, data: enrichedProfile, }; }, options, } satisfies OAuthProvider<AppleProfile>; }; export const getApplePublicKey = async (kid: string) => { const APPLE_BASE_URL = "https://appleid.apple.com"; const JWKS_APPLE_URI = "/auth/keys"; const { data } = await betterFetch<{ keys: Array<{ kid: string; alg: string; kty: string; use: string; n: string; e: string; }>; }>(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`); if (!data?.keys) { throw new APIError("BAD_REQUEST", { message: "Keys not found", }); } const jwk = data.keys.find((key) => key.kid === kid); if (!jwk) { throw new Error(`JWK with kid ${kid} not found`); } return await importJWK(jwk, jwk.alg); }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/test-utils/index.ts: -------------------------------------------------------------------------------- ```typescript import { afterAll } from "vitest"; import { betterAuth } from "../auth"; import { createAuthClient } from "../client/vanilla"; import type { BetterAuthOptions, Session, User } from "../types"; import { getMigrations } from "../db/get-migration"; import { parseSetCookieHeader, setCookieToHeader } from "../cookies"; import type { SuccessContext } from "@better-fetch/fetch"; import { getAdapter } from "../db/utils"; import { getBaseURL } from "../utils/url"; import { Kysely, MysqlDialect, PostgresDialect, sql } from "kysely"; import { Pool } from "pg"; import { MongoClient } from "mongodb"; import { mongodbAdapter } from "../adapters/mongodb-adapter"; import { createPool } from "mysql2/promise"; import { bearer } from "../plugins"; import type { BetterAuthClientOptions } from "@better-auth/core"; export async function getTestInstanceMemory< O extends Partial<BetterAuthOptions>, C extends BetterAuthClientOptions, >( options?: O, config?: { clientOptions?: C; port?: number; disableTestUser?: boolean; testUser?: Partial<User>; testWith?: "sqlite" | "postgres" | "mongodb" | "mysql" | "memory"; }, ) { const testWith = config?.testWith || "memory"; const postgres = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: "postgres://user:password@localhost:5432/better_auth", }), }), }); const mysql = new Kysely({ dialect: new MysqlDialect( createPool("mysql://user:password@localhost:3306/better_auth"), ), }); async function mongodbClient() { const dbClient = async (connectionString: string, dbName: string) => { const client = new MongoClient(connectionString); await client.connect(); const db = client.db(dbName); return db; }; const db = await dbClient("mongodb://127.0.0.1:27017", "better-auth"); return db; } const opts = { socialProviders: { github: { clientId: "test", clientSecret: "test", }, google: { clientId: "test", clientSecret: "test", }, }, secret: "better-auth.secret", database: testWith === "postgres" ? { db: postgres, type: "postgres" } : testWith === "mongodb" ? mongodbAdapter(await mongodbClient()) : testWith === "mysql" ? { db: mysql, type: "mysql" } : undefined, emailAndPassword: { enabled: true, }, rateLimit: { enabled: false, }, advanced: { cookies: {}, }, } satisfies BetterAuthOptions; const auth = betterAuth({ baseURL: "http://localhost:" + (config?.port || 3000), ...opts, ...options, advanced: { disableCSRFCheck: true, ...options?.advanced, }, plugins: [bearer(), ...(options?.plugins || [])], } as unknown as O extends undefined ? typeof opts : O & typeof opts); const testUser = { email: "[email protected]", password: "test123456", name: "test user", ...config?.testUser, }; async function createTestUser() { if (config?.disableTestUser) { return; } //@ts-expect-error const res = await auth.api.signUpEmail({ body: testUser, }); } if (testWith !== "mongodb" && testWith !== "memory") { const { runMigrations } = await getMigrations({ ...auth.options, database: opts.database, }); await runMigrations(); } await createTestUser(); afterAll(async () => { if (testWith === "mongodb") { const db = await mongodbClient(); await db.dropDatabase(); return; } if (testWith === "postgres") { await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute( postgres, ); await postgres.destroy(); return; } if (testWith === "mysql") { await sql`SET FOREIGN_KEY_CHECKS = 0;`.execute(mysql); const tables = await mysql.introspection.getTables(); for (const table of tables) { // @ts-expect-error await mysql.deleteFrom(table.name).execute(); } await sql`SET FOREIGN_KEY_CHECKS = 1;`.execute(mysql); return; } }); async function signInWithTestUser() { if (config?.disableTestUser) { throw new Error("Test user is disabled"); } let headers = new Headers(); const setCookie = (name: string, value: string) => { const current = headers.get("cookie"); headers.set("cookie", `${current || ""}; ${name}=${value}`); }; //@ts-expect-error const { data, error } = await client.signIn.email({ email: testUser.email, password: testUser.password, fetchOptions: { //@ts-expect-error onSuccess(context) { const header = context.response.headers.get("set-cookie"); const cookies = parseSetCookieHeader(header || ""); const signedCookie = cookies.get("better-auth.session_token")?.value; headers.set("cookie", `better-auth.session_token=${signedCookie}`); }, }, }); return { session: data.session as Session, user: data.user as User, headers, setCookie, }; } async function signInWithUser(email: string, password: string) { let headers = new Headers(); //@ts-expect-error const { data } = await client.signIn.email({ email, password, fetchOptions: { //@ts-expect-error onSuccess(context) { const header = context.response.headers.get("set-cookie"); const cookies = parseSetCookieHeader(header || ""); const signedCookie = cookies.get("better-auth.session_token")?.value; headers.set("cookie", `better-auth.session_token=${signedCookie}`); }, }, }); return { res: data as { user: User; session: Session; }, headers, }; } const customFetchImpl = async ( url: string | URL | Request, init?: RequestInit, ) => { return auth.handler(new Request(url, init)); }; function sessionSetter(headers: Headers) { return (context: SuccessContext) => { const header = context.response.headers.get("set-cookie"); if (header) { const cookies = parseSetCookieHeader(header || ""); const signedCookie = cookies.get("better-auth.session_token")?.value; headers.set("cookie", `better-auth.session_token=${signedCookie}`); } }; } const client = createAuthClient({ ...(config?.clientOptions as C extends undefined ? {} : C), baseURL: getBaseURL( options?.baseURL || "http://localhost:" + (config?.port || 3000), options?.basePath || "/api/auth", ), fetchOptions: { customFetchImpl, }, }); return { auth, client, testUser, signInWithTestUser, signInWithUser, cookieSetter: setCookieToHeader, customFetchImpl, sessionSetter, db: await getAdapter(auth.options), }; } ``` -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"; import { cva } from "class-variance-authority"; import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function NavigationMenu({ className, children, viewport = true, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & { viewport?: boolean; }) { return ( <NavigationMenuPrimitive.Root data-slot="navigation-menu" data-viewport={viewport} className={cn( "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", className, )} {...props} > {children} {viewport && <NavigationMenuViewport />} </NavigationMenuPrimitive.Root> ); } function NavigationMenuList({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) { return ( <NavigationMenuPrimitive.List data-slot="navigation-menu-list" className={cn( "group flex flex-1 list-none items-center justify-center gap-1", className, )} {...props} /> ); } function NavigationMenuItem({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) { return ( <NavigationMenuPrimitive.Item data-slot="navigation-menu-item" className={cn("relative", className)} {...props} /> ); } const navigationMenuTriggerStyle = cva( "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1", ); function NavigationMenuTrigger({ className, children, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) { return ( <NavigationMenuPrimitive.Trigger data-slot="navigation-menu-trigger" className={cn(navigationMenuTriggerStyle(), "group", className)} {...props} > {children}{" "} <ChevronDownIcon className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180" aria-hidden="true" /> </NavigationMenuPrimitive.Trigger> ); } function NavigationMenuContent({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) { return ( <NavigationMenuPrimitive.Content data-slot="navigation-menu-content" className={cn( "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto", "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", className, )} {...props} /> ); } function NavigationMenuViewport({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) { return ( <div className={cn( "absolute top-full left-0 isolate z-50 flex justify-center", )} > <NavigationMenuPrimitive.Viewport data-slot="navigation-menu-viewport" className={cn( "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]", className, )} {...props} /> </div> ); } function NavigationMenuLink({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) { return ( <NavigationMenuPrimitive.Link data-slot="navigation-menu-link" className={cn( "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} /> ); } function NavigationMenuIndicator({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) { return ( <NavigationMenuPrimitive.Indicator data-slot="navigation-menu-indicator" className={cn( "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden", className, )} {...props} > <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" /> </NavigationMenuPrimitive.Indicator> ); } export { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuContent, NavigationMenuTrigger, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, navigationMenuTriggerStyle, }; ``` -------------------------------------------------------------------------------- /docs/content/docs/concepts/email.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Email description: Learn how to use email with Better Auth. --- Email is a key part of Better Auth, required for all users regardless of their authentication method. Better Auth provides email and password authentication out of the box, and a lot of utilities to help you manage email verification, password reset, and more. ## Email Verification Email verification is a security feature that ensures users provide a valid email address. It helps prevent spam and abuse by confirming that the email address belongs to the user. In this guide, you'll get a walk through of how to implement token based email verification in your app. To use otp based email verification, check out the [OTP Verification](/docs/plugins/email-otp) guide. ### Adding Email Verification to Your App To enable email verification, you need to pass a function that sends a verification email with a link. - **sendVerificationEmail**: This function is triggered when email verification starts. It accepts a data object with the following properties: - `user`: The user object containing the email address. - `url`: The verification URL the user must click to verify their email. - `token`: The verification token used to complete the email verification to be used when implementing a custom verification URL. and a `request` object as the second parameter. ```ts title="auth.ts" import { betterAuth } from 'better-auth'; import { sendEmail } from './email'; // your email sending function export const auth = betterAuth({ emailVerification: { sendVerificationEmail: async ({ user, url, token }, request) => { await sendEmail({ to: user.email, subject: 'Verify your email address', text: `Click the link to verify your email: ${url}` }) } } }) ``` ### Triggering Email Verification You can initiate email verification in several ways: #### 1. During Sign-up To automatically send a verification email at signup, set `emailVerification.sendOnSignUp` to `true`. ```ts title="auth.ts" import { betterAuth } from 'better-auth'; export const auth = betterAuth({ emailVerification: { sendOnSignUp: true } }) ``` This sends a verification email when a user signs up. For social logins, email verification status is read from the SSO. <Callout> With `sendOnSignUp` enabled, when the user logs in with an SSO that does not claim the email as verified, Better Auth will dispatch a verification email, but the verification is not required to login even when `requireEmailVerification` is enabled. </Callout> #### 2. Require Email Verification If you enable require email verification, users must verify their email before they can log in. And every time a user tries to sign in, `sendVerificationEmail` is called. <Callout> This only works if you have `sendVerificationEmail` implemented and if the user is trying to sign in with email and password. </Callout> ```ts title="auth.ts" export const auth = betterAuth({ emailAndPassword: { requireEmailVerification: true } }) ``` if a user tries to sign in without verifying their email, you can handle the error and show a message to the user. ```ts title="auth-client.ts" await authClient.signIn.email({ email: "[email protected]", password: "password" }, { onError: (ctx) => { // Handle the error if(ctx.error.status === 403) { alert("Please verify your email address") } //you can also show the original error message alert(ctx.error.message) } }) ``` #### 3. Manually You can also manually trigger email verification by calling `sendVerificationEmail`. ```ts await authClient.sendVerificationEmail({ email: "[email protected]", callbackURL: "/" // The redirect URL after verification }) ``` ### Verifying the Email If the user clicks the provided verification URL, their email is automatically verified, and they are redirected to the `callbackURL`. For manual verification, you can send the user a custom link with the `token` and call the `verifyEmail` function. ```ts await authClient.verifyEmail({ query: { token: "" // Pass the token here } }) ``` ### Auto Sign In After Verification To sign in the user automatically after they successfully verify their email, set the `autoSignInAfterVerification` option to `true`: ```ts const auth = betterAuth({ //...your other options emailVerification: { autoSignInAfterVerification: true } }) ``` ### Callback after successful email verification You can run custom code immediately after a user verifies their email using the `afterEmailVerification` callback. This is useful for any side-effects you want to trigger, like granting access to special features or logging the event. The `afterEmailVerification` function runs automatically when a user's email is confirmed, receiving the `user` object and `request` details so you can perform actions for that specific user. Here's how you can set it up: ```ts title="auth.ts" import { betterAuth } from 'better-auth'; export const auth = betterAuth({ emailVerification: { async afterEmailVerification(user, request) { // Your custom logic here, e.g., grant access to premium features console.log(`${user.email} has been successfully verified!`); } } }) ``` ## Password Reset Email Password reset allows users to reset their password if they forget it. Better Auth provides a simple way to implement password reset functionality. You can enable password reset by passing a function that sends a password reset email with a link. ```ts title="auth.ts" import { betterAuth } from 'better-auth'; import { sendEmail } from './email'; // your email sending function export const auth = betterAuth({ emailAndPassword: { enabled: true, sendResetPassword: async ({ user, url, token }, request) => { await sendEmail({ to: user.email, subject: 'Reset your password', text: `Click the link to reset your password: ${url}` }) } } }) ``` Check out the [Email and Password](/docs/authentication/email-password#forget-password) guide for more details on how to implement password reset in your app. Also you can check out the [Otp verification](/docs/plugins/email-otp#reset-password) guide for how to implement password reset with OTP in your app. ``` -------------------------------------------------------------------------------- /docs/components/docs/page.tsx: -------------------------------------------------------------------------------- ```typescript import type { TableOfContents } from "fumadocs-core/server"; import { type AnchorHTMLAttributes, forwardRef, type HTMLAttributes, type ReactNode, } from "react"; import { type AnchorProviderProps, AnchorProvider } from "fumadocs-core/toc"; import { replaceOrDefault } from "./shared"; import { cn } from "../../lib/utils"; import { Footer, type FooterProps, LastUpdate, TocPopoverHeader, type BreadcrumbProps, PageBody, PageArticle, } from "./page.client"; import { Toc, TOCItems, TocPopoverTrigger, TocPopoverContent, type TOCProps, TOCScrollArea, } from "./layout/toc"; import { buttonVariants } from "./ui/button"; import { Edit, Text } from "lucide-react"; import { I18nLabel } from "fumadocs-ui/provider"; type TableOfContentOptions = Omit<TOCProps, "items" | "children"> & Pick<AnchorProviderProps, "single"> & { enabled: boolean; component: ReactNode; }; type TableOfContentPopoverOptions = Omit<TableOfContentOptions, "single">; interface EditOnGitHubOptions extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "children"> { owner: string; repo: string; /** * SHA or ref (branch or tag) name. * * @defaultValue main */ sha?: string; /** * File path in the repo */ path: string; } interface BreadcrumbOptions extends BreadcrumbProps { enabled: boolean; component: ReactNode; /** * Show the full path to the current page * * @defaultValue false * @deprecated use `includePage` instead */ full?: boolean; } interface FooterOptions extends FooterProps { enabled: boolean; component: ReactNode; } export interface DocsPageProps { toc?: TableOfContents; /** * Extend the page to fill all available space * * @defaultValue false */ full?: boolean; tableOfContent?: Partial<TableOfContentOptions>; tableOfContentPopover?: Partial<TableOfContentPopoverOptions>; /** * Replace or disable breadcrumb */ breadcrumb?: Partial<BreadcrumbOptions>; /** * Footer navigation, you can disable it by passing `false` */ footer?: Partial<FooterOptions>; editOnGithub?: EditOnGitHubOptions; lastUpdate?: Date | string | number; container?: HTMLAttributes<HTMLDivElement>; article?: HTMLAttributes<HTMLElement>; children: ReactNode; } export function DocsPage({ toc = [], full = false, tableOfContentPopover: { enabled: tocPopoverEnabled, component: tocPopoverReplace, ...tocPopoverOptions } = {}, tableOfContent: { enabled: tocEnabled, component: tocReplace, ...tocOptions } = {}, ...props }: DocsPageProps) { const isTocRequired = toc.length > 0 || tocOptions.footer !== undefined || tocOptions.header !== undefined; // disable TOC on full mode, you can still enable it with `enabled` option. tocEnabled ??= !full && isTocRequired; tocPopoverEnabled ??= toc.length > 0 || tocPopoverOptions.header !== undefined || tocPopoverOptions.footer !== undefined; return ( <AnchorProvider toc={toc} single={tocOptions.single}> <PageBody {...props.container} className={cn(props.container?.className)} style={ { "--fd-tocnav-height": !tocPopoverEnabled ? "0px" : undefined, ...props.container?.style, } as object } > {replaceOrDefault( { enabled: tocPopoverEnabled, component: tocPopoverReplace }, <TocPopoverHeader className="h-10"> <TocPopoverTrigger className="w-full" items={toc} /> <TocPopoverContent> {tocPopoverOptions.header} <TOCScrollArea isMenu> <TOCItems items={toc} /> </TOCScrollArea> {tocPopoverOptions.footer} </TocPopoverContent> </TocPopoverHeader>, { items: toc, ...tocPopoverOptions, }, )} <PageArticle {...props.article} className={cn( full || !tocEnabled ? "max-w-[1120px]" : "max-w-[860px]", props.article?.className, )} > {props.children} <div role="none" className="flex-1" /> <div className="flex flex-row flex-wrap items-center justify-between gap-4 empty:hidden"> {props.editOnGithub ? ( <EditOnGitHub {...props.editOnGithub} /> ) : null} {props.lastUpdate ? ( <LastUpdate date={new Date(props.lastUpdate)} /> ) : null} </div> {replaceOrDefault( props.footer, <Footer items={props.footer?.items} />, )} </PageArticle> </PageBody> {replaceOrDefault( { enabled: tocEnabled, component: tocReplace }, <Toc> {tocOptions.header} <h3 className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"> <Text className="size-4" /> <I18nLabel label="toc" /> </h3> <TOCScrollArea> <TOCItems items={toc} /> </TOCScrollArea> {tocOptions.footer} </Toc>, { items: toc, ...tocOptions, }, )} </AnchorProvider> ); } function EditOnGitHub({ owner, repo, sha, path, ...props }: EditOnGitHubOptions) { const href = `https://github.com/${owner}/${repo}/blob/${sha}/${path.startsWith("/") ? path.slice(1) : path}`; return ( <a href={href} target="_blank" rel="noreferrer noopener" {...props} className={cn( buttonVariants({ color: "secondary", className: "gap-1.5 text-fd-muted-foreground", }), props.className, )} > <Edit className="size-3.5" /> <I18nLabel label="editOnGithub" /> </a> ); } /** * Add typography styles */ export const DocsBody = forwardRef< HTMLDivElement, HTMLAttributes<HTMLDivElement> >((props, ref) => ( <div ref={ref} {...props} className={cn("prose", props.className)}> {props.children} </div> )); DocsBody.displayName = "DocsBody"; export const DocsDescription = forwardRef< HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement> >((props, ref) => { // don't render if no description provided if (props.children === undefined) return null; return ( <p ref={ref} {...props} className={cn("mb-8 text-lg text-fd-muted-foreground", props.className)} > {props.children} </p> ); }); DocsDescription.displayName = "DocsDescription"; export const DocsTitle = forwardRef< HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement> >((props, ref) => { return ( <h1 ref={ref} {...props} className={cn("text-3xl font-semibold", props.className)} > {props.children} </h1> ); }); DocsTitle.displayName = "DocsTitle"; /** * For separate MDX page */ export function withArticle({ children }: { children: ReactNode }): ReactNode { return ( <main className="container py-12"> <article className="prose">{children}</article> </main> ); } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/context-menu.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; import { CheckIcon, ChevronRightIcon, DotFilledIcon, } from "@radix-ui/react-icons"; import { cn } from "@/lib/utils"; const ContextMenu = ContextMenuPrimitive.Root; const ContextMenuTrigger = ContextMenuPrimitive.Trigger; const ContextMenuGroup = ContextMenuPrimitive.Group; const ContextMenuPortal = ContextMenuPrimitive.Portal; const ContextMenuSub = ContextMenuPrimitive.Sub; const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; const ContextMenuSubTrigger = ({ ref, className, inset, children, ...props }) => ( <ContextMenuPrimitive.SubTrigger ref={ref} className={cn( "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", inset && "pl-8", className, )} {...props} > {children} <ChevronRightIcon className="ml-auto h-4 w-4" /> </ContextMenuPrimitive.SubTrigger> ); ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; const ContextMenuSubContent = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> & { ref: React.RefObject< React.ElementRef<typeof ContextMenuPrimitive.SubContent> >; }) => ( <ContextMenuPrimitive.SubContent ref={ref} className={cn( "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className, )} {...props} /> ); ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; const ContextMenuContent = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> & { ref: React.RefObject<React.ElementRef<typeof ContextMenuPrimitive.Content>>; }) => ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Content ref={ref} className={cn( "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className, )} {...props} /> </ContextMenuPrimitive.Portal> ); ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; const ContextMenuItem = ({ ref, className, inset, ...props }) => ( <ContextMenuPrimitive.Item ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", inset && "pl-8", className, )} {...props} /> ); ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; const ContextMenuCheckboxItem = ({ ref, className, children, checked, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & { ref: React.RefObject< React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem> >; }) => ( <ContextMenuPrimitive.CheckboxItem ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", className, )} checked={checked} {...props} > <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <CheckIcon className="h-4 w-4" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.CheckboxItem> ); ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; const ContextMenuRadioItem = ({ ref, className, children, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & { ref: React.RefObject<React.ElementRef<typeof ContextMenuPrimitive.RadioItem>>; }) => ( <ContextMenuPrimitive.RadioItem ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", className, )} {...props} > <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <DotFilledIcon className="h-4 w-4 fill-current" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.RadioItem> ); ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; const ContextMenuLabel = ({ ref, className, inset, ...props }) => ( <ContextMenuPrimitive.Label ref={ref} className={cn( "px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className, )} {...props} /> ); ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; const ContextMenuSeparator = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> & { ref: React.RefObject<React.ElementRef<typeof ContextMenuPrimitive.Separator>>; }) => ( <ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} /> ); ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { return ( <span className={cn( "ml-auto text-xs tracking-widest text-muted-foreground", className, )} {...props} /> ); }; ContextMenuShortcut.displayName = "ContextMenuShortcut"; export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup, }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/memory-adapter/memory-adapter.ts: -------------------------------------------------------------------------------- ```typescript import { logger } from "@better-auth/core/env"; import { createAdapterFactory } from "../adapter-factory"; import type { BetterAuthOptions } from "@better-auth/core"; import type { DBAdapterDebugLogOption, CleanedWhere, } from "@better-auth/core/db/adapter"; export interface MemoryDB { [key: string]: any[]; } export interface MemoryAdapterConfig { debugLogs?: DBAdapterDebugLogOption; } export const memoryAdapter = (db: MemoryDB, config?: MemoryAdapterConfig) => { let lazyOptions: BetterAuthOptions | null = null; let adapterCreator = createAdapterFactory({ config: { adapterId: "memory", adapterName: "Memory Adapter", usePlural: false, debugLogs: config?.debugLogs || false, customTransformInput(props) { if ( props.options.advanced?.database?.useNumberId && props.field === "id" && props.action === "create" ) { return db[props.model]!.length + 1; } return props.data; }, transaction: async (cb) => { let clone = structuredClone(db); try { const r = await cb(adapterCreator(lazyOptions!)); return r; } catch (error) { // Rollback changes Object.keys(db).forEach((key) => { db[key] = clone[key]!; }); throw error; } }, }, adapter: ({ getFieldName, options, debugLog }) => { function convertWhereClause(where: CleanedWhere[], model: string) { const table = db[model]; if (!table) { logger.error( `[MemoryAdapter] Model ${model} not found in the DB`, Object.keys(db), ); throw new Error(`Model ${model} not found`); } const evalClause = (record: any, clause: CleanedWhere): boolean => { const { field, value, operator } = clause; switch (operator) { case "in": if (!Array.isArray(value)) { throw new Error("Value must be an array"); } // @ts-expect-error return value.includes(record[field]); case "not_in": if (!Array.isArray(value)) { throw new Error("Value must be an array"); } // @ts-expect-error return !value.includes(record[field]); case "contains": return record[field].includes(value); case "starts_with": return record[field].startsWith(value); case "ends_with": return record[field].endsWith(value); case "ne": return record[field] !== value; case "gt": return value != null && Boolean(record[field] > value); case "gte": return value != null && Boolean(record[field] >= value); case "lt": return value != null && Boolean(record[field] < value); case "lte": return value != null && Boolean(record[field] <= value); default: return record[field] === value; } }; return table.filter((record: any) => { if (!where.length || where.length === 0) { return true; } let result = evalClause(record, where[0]!); for (const clause of where) { const clauseResult = evalClause(record, clause); if (clause.connector === "OR") { result = result || clauseResult; } else { result = result && clauseResult; } } return result; }); } return { create: async ({ model, data }) => { if (options.advanced?.database?.useNumberId) { // @ts-expect-error data.id = db[model]!.length + 1; } if (!db[model]) { db[model] = []; } db[model]!.push(data); return data; }, findOne: async ({ model, where }) => { const res = convertWhereClause(where, model); const record = res[0] || null; return record; }, findMany: async ({ model, where, sortBy, limit, offset }) => { let table = db[model]; if (where) { table = convertWhereClause(where, model); } if (sortBy) { table = table!.sort((a, b) => { const field = getFieldName({ model, field: sortBy.field }); const aValue = a[field]; const bValue = b[field]; let comparison = 0; // Handle null/undefined values if (aValue == null && bValue == null) { comparison = 0; } else if (aValue == null) { comparison = -1; } else if (bValue == null) { comparison = 1; } // Handle string comparison else if ( typeof aValue === "string" && typeof bValue === "string" ) { comparison = aValue.localeCompare(bValue); } // Handle date comparison else if (aValue instanceof Date && bValue instanceof Date) { comparison = aValue.getTime() - bValue.getTime(); } // Handle numeric comparison else if ( typeof aValue === "number" && typeof bValue === "number" ) { comparison = aValue - bValue; } // Handle boolean comparison else if ( typeof aValue === "boolean" && typeof bValue === "boolean" ) { comparison = aValue === bValue ? 0 : aValue ? 1 : -1; } // Fallback to string comparison else { comparison = String(aValue).localeCompare(String(bValue)); } return sortBy.direction === "asc" ? comparison : -comparison; }); } if (offset !== undefined) { table = table!.slice(offset); } if (limit !== undefined) { table = table!.slice(0, limit); } return table || []; }, count: async ({ model, where }) => { if (where) { const filteredRecords = convertWhereClause(where, model); return filteredRecords.length; } return db[model]!.length; }, update: async ({ model, where, update }) => { const res = convertWhereClause(where, model); res.forEach((record) => { Object.assign(record, update); }); return res[0] || null; }, delete: async ({ model, where }) => { const table = db[model]!; const res = convertWhereClause(where, model); db[model] = table.filter((record) => !res.includes(record)); }, deleteMany: async ({ model, where }) => { const table = db[model]!; const res = convertWhereClause(where, model); let count = 0; db[model] = table.filter((record) => { if (res.includes(record)) { count++; return false; } return !res.includes(record); }); return count; }, updateMany({ model, where, update }) { const res = convertWhereClause(where, model); res.forEach((record) => { Object.assign(record, update); }); return res[0] || null; }, }; }, }); return (options: BetterAuthOptions) => { lazyOptions = options; return adapterCreator(options); }; }; ``` -------------------------------------------------------------------------------- /demo/nextjs/components/blocks/pricing.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Button, buttonVariants } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; import { motion } from "framer-motion"; import { Star } from "lucide-react"; import { useState, useRef, useEffect } from "react"; import confetti from "canvas-confetti"; import NumberFlow from "@number-flow/react"; import { CheckIcon } from "@radix-ui/react-icons"; import { client } from "@/lib/auth-client"; function useMediaQuery(query: string) { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); if (media.matches !== matches) { setMatches(media.matches); } const listener = () => setMatches(media.matches); media.addListener(listener); return () => media.removeListener(listener); }, [query]); return matches; } interface PricingPlan { name: string; price: string; yearlyPrice: string; period: string; features: string[]; description: string; buttonText: string; href: string; isPopular: boolean; } interface PricingProps { plans: PricingPlan[]; title?: string; description?: string; } export function Pricing({ plans, title = "Simple, Transparent Pricing", description = "Choose the plan that works for you", }: PricingProps) { const [isMonthly, setIsMonthly] = useState(true); const isDesktop = useMediaQuery("(min-width: 768px)"); const switchRef = useRef<HTMLButtonElement>(null); const handleToggle = (checked: boolean) => { setIsMonthly(!checked); if (checked && switchRef.current) { const rect = switchRef.current.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; confetti({ particleCount: 50, spread: 60, origin: { x: x / window.innerWidth, y: y / window.innerHeight, }, colors: [ "hsl(var(--primary))", "hsl(var(--accent))", "hsl(var(--secondary))", "hsl(var(--muted))", ], ticks: 200, gravity: 1.2, decay: 0.94, startVelocity: 30, shapes: ["circle"], }); } }; return ( <div className="container py-4"> <div className="text-center space-y-4 mb-3"> <h2 className="text-2xl font-bold tracking-tight sm:text-3xl"> {title} </h2> <p className="text-muted-foreground whitespace-pre-line"> {description} </p> </div> <div className="flex justify-center mb-10"> <label className="relative inline-flex items-center cursor-pointer"> <Label> <Switch ref={switchRef as any} checked={!isMonthly} onCheckedChange={handleToggle} className="relative" /> </Label> </label> <span className="ml-2 font-semibold"> Annual billing <span className="text-primary">(Save 20%)</span> </span> </div> <div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4"> {plans.map((plan, index) => ( <motion.div key={index} initial={{ y: 50, opacity: 1 }} whileInView={ isDesktop ? { y: plan.isPopular ? -20 : 0, opacity: 1, x: index === 2 ? -30 : index === 0 ? 30 : 0, scale: index === 0 || index === 2 ? 0.94 : 1.0, } : {} } viewport={{ once: true }} transition={{ duration: 1.6, type: "spring", stiffness: 100, damping: 30, delay: 0.4, opacity: { duration: 0.5 }, }} className={cn( `rounded-sm border p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`, plan.isPopular ? "border-border border-2" : "border-border", "flex flex-col", !plan.isPopular && "mt-5", index === 0 || index === 2 ? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-10" : "z-10", index === 0 && "origin-right", index === 2 && "origin-left", )} > {plan.isPopular && ( <div className="absolute top-0 right-0 bg-primary py-0.5 px-2 rounded-bl-sm rounded-tr-sm flex items-center"> <Star className="text-primary-foreground h-4 w-4 fill-current" /> <span className="text-primary-foreground ml-1 font-sans font-semibold"> Popular </span> </div> )} <div className="flex-1 flex flex-col"> <p className="text-base font-semibold text-muted-foreground mt-2"> {plan.name} </p> <div className="mt-6 flex items-center justify-center gap-x-2"> <span className="text-5xl font-bold tracking-tight text-foreground"> <NumberFlow value={ isMonthly ? Number(plan.price) : Number(plan.yearlyPrice) } format={{ style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0, }} transformTiming={{ duration: 500, easing: "ease-out", }} willChange className="font-variant-numeric: tabular-nums" /> </span> {plan.period !== "Next 3 months" && ( <span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground"> / {plan.period} </span> )} </div> <p className="text-xs leading-5 text-muted-foreground"> {isMonthly ? "billed monthly" : "billed annually"} </p> <ul className="mt-5 gap-2 flex flex-col"> {plan.features.map((feature, idx) => ( <li key={idx} className="flex items-start gap-2"> <CheckIcon className="h-4 w-4 text-primary mt-1 shrink-0" /> <span className="text-left">{feature}</span> </li> ))} </ul> <hr className="w-full my-4" /> <Button onClick={async () => { await client.subscription.upgrade({ plan: plan.name.toLowerCase(), successUrl: "/dashboard", }); }} className={cn( buttonVariants({ variant: "outline", }), "group relative w-full gap-2 overflow-hidden text-lg font-semibold tracking-tighter", "transform-gpu ring-offset-current transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-1 hover:bg-primary hover:text-primary-foreground", plan.isPopular ? "bg-primary text-primary-foreground" : "bg-background text-foreground", )} > {plan.buttonText} </Button> <p className="mt-6 text-xs leading-5 text-muted-foreground"> {plan.description} </p> </div> </motion.div> ))} </div> </div> ); } ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/cognito.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose"; import { BetterAuthError } from "../error"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2"; import { logger } from "../env"; import { refreshAccessToken } from "../oauth2"; import { APIError } from "better-call"; export interface CognitoProfile { sub: string; email: string; email_verified: boolean; name: string; given_name?: string; family_name?: string; picture?: string; username?: string; locale?: string; phone_number?: string; phone_number_verified?: boolean; aud: string; iss: string; exp: number; iat: number; // Custom attributes from Cognito can be added here [key: string]: any; } export interface CognitoOptions extends ProviderOptions<CognitoProfile> { clientId: string; /** * The Cognito domain (e.g., "your-app.auth.us-east-1.amazoncognito.com") */ domain: string; /** * AWS region where User Pool is hosted (e.g., "us-east-1") */ region: string; userPoolId: string; requireClientSecret?: boolean; } export const cognito = (options: CognitoOptions) => { if (!options.domain || !options.region || !options.userPoolId) { logger.error( "Domain, region and userPoolId are required for Amazon Cognito. Make sure to provide them in the options.", ); throw new BetterAuthError("DOMAIN_AND_REGION_REQUIRED"); } const cleanDomain = options.domain.replace(/^https?:\/\//, ""); const authorizationEndpoint = `https://${cleanDomain}/oauth2/authorize`; const tokenEndpoint = `https://${cleanDomain}/oauth2/token`; const userInfoEndpoint = `https://${cleanDomain}/oauth2/userinfo`; return { id: "cognito", name: "Cognito", async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { if (!options.clientId) { logger.error( "ClientId is required for Amazon Cognito. Make sure to provide them in the options.", ); throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED"); } if (options.requireClientSecret && !options.clientSecret) { logger.error( "Client Secret is required when requireClientSecret is true. Make sure to provide it in the options.", ); throw new BetterAuthError("CLIENT_SECRET_REQUIRED"); } const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); const url = await createAuthorizationURL({ id: "cognito", options: { ...options, }, authorizationEndpoint, scopes: _scopes, state, codeVerifier, redirectURI, prompt: options.prompt, }); return url; }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { return validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint, }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint, }); }, async verifyIdToken(token, nonce) { if (options.disableIdTokenSignIn) { return false; } if (options.verifyIdToken) { return options.verifyIdToken(token, nonce); } try { const decodedHeader = decodeProtectedHeader(token); const { kid, alg: jwtAlg } = decodedHeader; if (!kid || !jwtAlg) return false; const publicKey = await getCognitoPublicKey( kid, options.region, options.userPoolId, ); const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`; const { payload: jwtClaims } = await jwtVerify(token, publicKey, { algorithms: [jwtAlg], issuer: expectedIssuer, audience: options.clientId, maxTokenAge: "1h", }); if (nonce && jwtClaims.nonce !== nonce) { return false; } return true; } catch (error) { logger.error("Failed to verify ID token:", error); return false; } }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } if (token.idToken) { try { const profile = decodeJwt<CognitoProfile>(token.idToken); if (!profile) { return null; } const name = profile.name || profile.given_name || profile.username || profile.email; const enrichedProfile = { ...profile, name, }; const userMap = await options.mapProfileToUser?.(enrichedProfile); return { user: { id: profile.sub, name: enrichedProfile.name, email: profile.email, image: profile.picture, emailVerified: profile.email_verified, ...userMap, }, data: enrichedProfile, }; } catch (error) { logger.error("Failed to decode ID token:", error); } } if (token.accessToken) { try { const { data: userInfo } = await betterFetch<CognitoProfile>( userInfoEndpoint, { headers: { Authorization: `Bearer ${token.accessToken}`, }, }, ); if (userInfo) { const userMap = await options.mapProfileToUser?.(userInfo); return { user: { id: userInfo.sub, name: userInfo.name || userInfo.given_name || userInfo.username, email: userInfo.email, image: userInfo.picture, emailVerified: userInfo.email_verified, ...userMap, }, data: userInfo, }; } } catch (error) { logger.error("Failed to fetch user info from Cognito:", error); } } return null; }, options, } satisfies OAuthProvider<CognitoProfile>; }; export const getCognitoPublicKey = async ( kid: string, region: string, userPoolId: string, ) => { const COGNITO_JWKS_URI = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`; try { const { data } = await betterFetch<{ keys: Array<{ kid: string; alg: string; kty: string; use: string; n: string; e: string; }>; }>(COGNITO_JWKS_URI); if (!data?.keys) { throw new APIError("BAD_REQUEST", { message: "Keys not found", }); } const jwk = data.keys.find((key) => key.kid === kid); if (!jwk) { throw new Error(`JWK with kid ${kid} not found`); } return await importJWK(jwk, jwk.alg); } catch (error) { logger.error("Failed to fetch Cognito public key:", error); throw error; } }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/types.ts: -------------------------------------------------------------------------------- ```typescript import type { InferOptionSchema } from "../../types"; import type { Statements } from "../access"; import type { apiKeySchema } from "./schema"; import type { GenericEndpointContext, HookEndpointContext, } from "@better-auth/core"; export interface ApiKeyOptions { /** * The header name to check for API key * @default "x-api-key" */ apiKeyHeaders?: string | string[]; /** * Disable hashing of the API key. * * ⚠️ Security Warning: It's strongly recommended to not disable hashing. * Storing API keys in plaintext makes them vulnerable to database breaches, potentially exposing all your users' API keys. * * @default false */ disableKeyHashing?: boolean; /** * The function to get the API key from the context */ customAPIKeyGetter?: (ctx: HookEndpointContext) => string | null; /** * A custom function to validate the API key */ customAPIKeyValidator?: (options: { ctx: GenericEndpointContext; key: string; }) => boolean | Promise<boolean>; /** * custom key generation function */ customKeyGenerator?: (options: { /** * The length of the API key to generate */ length: number; /** * The prefix of the API key to generate */ prefix: string | undefined; }) => string | Promise<string>; /** * The configuration for storing the starting characters of the API key in the database. * * Useful if you want to display the starting characters of an API key in the UI. */ startingCharactersConfig?: { /** * Whether to store the starting characters in the database. If false, we will set `start` to `null`. * * @default true */ shouldStore?: boolean; /** * The length of the starting characters to store in the database. * * This includes the prefix length. * * @default 6 */ charactersLength?: number; }; /** * The length of the API key. Longer is better. Default is 64. (Doesn't include the prefix length) * @default 64 */ defaultKeyLength?: number; /** * The prefix of the API key. * * Note: We recommend you append an underscore to the prefix to make the prefix more identifiable. (eg `hello_`) */ defaultPrefix?: string; /** * The maximum length of the prefix. * * @default 32 */ maximumPrefixLength?: number; /** * Whether to require a name for the API key. * * @default false */ requireName?: boolean; /** * The minimum length of the prefix. * * @default 1 */ minimumPrefixLength?: number; /** * The maximum length of the name. * * @default 32 */ maximumNameLength?: number; /** * The minimum length of the name. * * @default 1 */ minimumNameLength?: number; /** * Whether to enable metadata for an API key. * * @default false */ enableMetadata?: boolean; /** * Customize the key expiration. */ keyExpiration?: { /** * The default expires time in milliseconds. * * If `null`, then there will be no expiration time. * * @default null */ defaultExpiresIn?: number | null; /** * Whether to disable the expires time passed from the client. * * If `true`, the expires time will be based on the default values. * * @default false */ disableCustomExpiresTime?: boolean; /** * The minimum expiresIn value allowed to be set from the client. in days. * * @default 1 */ minExpiresIn?: number; /** * The maximum expiresIn value allowed to be set from the client. in days. * * @default 365 */ maxExpiresIn?: number; }; /** * Default rate limiting options. */ rateLimit?: { /** * Whether to enable rate limiting. * * @default true */ enabled?: boolean; /** * The duration in milliseconds where each request is counted. * * Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. * * @default 1000 * 60 * 60 * 24 // 1 day */ timeWindow?: number; /** * Maximum amount of requests allowed within a window * * Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. * * @default 10 // 10 requests per day */ maxRequests?: number; }; /** * custom schema for the API key plugin */ schema?: InferOptionSchema<ReturnType<typeof apiKeySchema>>; /** * An API Key can represent a valid session, so we automatically mock a session for the user if we find a valid API key in the request headers. * * ⚠︎ This is not recommended for production use, as it can lead to security issues. * @default false */ enableSessionForAPIKeys?: boolean; /** * Permissions for the API key. */ permissions?: { /** * The default permissions for the API key. */ defaultPermissions?: | Statements | (( userId: string, ctx: GenericEndpointContext, ) => Statements | Promise<Statements>); }; } export type ApiKey = { /** * ID */ id: string; /** * The name of the key */ name: string | null; /** * Shows the first few characters of the API key, including the prefix. * This allows you to show those few characters in the UI to make it easier for users to identify the API key. */ start: string | null; /** * The API Key prefix. Stored as plain text. */ prefix: string | null; /** * The hashed API key value */ key: string; /** * The owner of the user id */ userId: string; /** * The interval in milliseconds between refills of the `remaining` count * * @example 3600000 // refill every hour (3600000ms = 1h) */ refillInterval: number | null; /** * The amount to refill */ refillAmount: number | null; /** * The last refill date */ lastRefillAt: Date | null; /** * Sets if key is enabled or disabled * * @default true */ enabled: boolean; /** * Whether the key has rate limiting enabled. */ rateLimitEnabled: boolean; /** * The duration in milliseconds */ rateLimitTimeWindow: number | null; /** * Maximum amount of requests allowed within a window */ rateLimitMax: number | null; /** * The number of requests made within the rate limit time window */ requestCount: number; /** * Remaining requests (every time API key is used this should updated and should be updated on refill as well) */ remaining: number | null; /** * When last request occurred */ lastRequest: Date | null; /** * Expiry date of a key */ expiresAt: Date | null; /** * created at */ createdAt: Date; /** * updated at */ updatedAt: Date; /** * Extra metadata about the apiKey */ metadata: Record<string, any> | null; /** * Permissions for the API key */ permissions?: { [key: string]: string[]; } | null; }; ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/paypal.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { BetterAuthError } from "../error"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { createAuthorizationURL } from "../oauth2"; import { logger } from "../env"; import { decodeJwt } from "jose"; import { base64 } from "@better-auth/utils/base64"; export interface PayPalProfile { user_id: string; name: string; given_name: string; family_name: string; middle_name?: string; picture?: string; email: string; email_verified: boolean; gender?: string; birthdate?: string; zoneinfo?: string; locale?: string; phone_number?: string; address?: { street_address?: string; locality?: string; region?: string; postal_code?: string; country?: string; }; verified_account?: boolean; account_type?: string; age_range?: string; payer_id?: string; } export interface PayPalTokenResponse { scope?: string; access_token: string; refresh_token?: string; token_type: "Bearer"; id_token?: string; expires_in: number; nonce?: string; } export interface PayPalOptions extends ProviderOptions<PayPalProfile> { clientId: string; /** * PayPal environment - 'sandbox' for testing, 'live' for production * @default 'sandbox' */ environment?: "sandbox" | "live"; /** * Whether to request shipping address information * @default false */ requestShippingAddress?: boolean; } export const paypal = (options: PayPalOptions) => { const environment = options.environment || "sandbox"; const isSandbox = environment === "sandbox"; const authorizationEndpoint = isSandbox ? "https://www.sandbox.paypal.com/signin/authorize" : "https://www.paypal.com/signin/authorize"; const tokenEndpoint = isSandbox ? "https://api-m.sandbox.paypal.com/v1/oauth2/token" : "https://api-m.paypal.com/v1/oauth2/token"; const userInfoEndpoint = isSandbox ? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo" : "https://api-m.paypal.com/v1/identity/oauth2/userinfo"; return { id: "paypal", name: "PayPal", async createAuthorizationURL({ state, codeVerifier, redirectURI }) { if (!options.clientId || !options.clientSecret) { logger.error( "Client Id and Client Secret is required for PayPal. Make sure to provide them in the options.", ); throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED"); } /** * Log in with PayPal doesn't use traditional OAuth2 scopes * Instead, permissions are configured in the PayPal Developer Dashboard * We don't pass any scopes to avoid "invalid scope" errors **/ const _scopes: string[] = []; const url = await createAuthorizationURL({ id: "paypal", options, authorizationEndpoint, scopes: _scopes, state, codeVerifier, redirectURI, prompt: options.prompt, }); return url; }, validateAuthorizationCode: async ({ code, redirectURI }) => { /** * PayPal requires Basic Auth for token exchange **/ const credentials = base64.encode( `${options.clientId}:${options.clientSecret}`, ); try { const response = await betterFetch(tokenEndpoint, { method: "POST", headers: { Authorization: `Basic ${credentials}`, Accept: "application/json", "Accept-Language": "en_US", "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", code: code, redirect_uri: redirectURI, }).toString(), }); if (!response.data) { throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN"); } const data = response.data as PayPalTokenResponse; const result = { accessToken: data.access_token, refreshToken: data.refresh_token, accessTokenExpiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1000) : undefined, idToken: data.id_token, }; return result; } catch (error) { logger.error("PayPal token exchange failed:", error); throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN"); } }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { const credentials = base64.encode( `${options.clientId}:${options.clientSecret}`, ); try { const response = await betterFetch(tokenEndpoint, { method: "POST", headers: { Authorization: `Basic ${credentials}`, Accept: "application/json", "Accept-Language": "en_US", "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, }).toString(), }); if (!response.data) { throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN"); } const data = response.data as any; return { accessToken: data.access_token, refreshToken: data.refresh_token, accessTokenExpiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1000) : undefined, }; } catch (error) { logger.error("PayPal token refresh failed:", error); throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN"); } }, async verifyIdToken(token, nonce) { if (options.disableIdTokenSignIn) { return false; } if (options.verifyIdToken) { return options.verifyIdToken(token, nonce); } try { const payload = decodeJwt(token); return !!payload.sub; } catch (error) { logger.error("Failed to verify PayPal ID token:", error); return false; } }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } if (!token.accessToken) { logger.error("Access token is required to fetch PayPal user info"); return null; } try { const response = await betterFetch<PayPalProfile>( `${userInfoEndpoint}?schema=paypalv1.1`, { headers: { Authorization: `Bearer ${token.accessToken}`, Accept: "application/json", }, }, ); if (!response.data) { logger.error("Failed to fetch user info from PayPal"); return null; } const userInfo = response.data; const userMap = await options.mapProfileToUser?.(userInfo); const result = { user: { id: userInfo.user_id, name: userInfo.name, email: userInfo.email, image: userInfo.picture, emailVerified: userInfo.email_verified, ...userMap, }, data: userInfo, }; return result; } catch (error) { logger.error("Failed to fetch user info from PayPal:", error); return null; } }, options, } satisfies OAuthProvider<PayPalProfile>; }; ``` -------------------------------------------------------------------------------- /docs/components/logo-context-menu.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import type React from "react"; import { useState, useRef, useEffect } from "react"; import { Code, Image, Type } from "lucide-react"; import { toast } from "sonner"; import { useTheme } from "next-themes"; import type { StaticImageData } from "next/image"; interface LogoAssets { darkSvg: string; whiteSvg: string; darkWordmark: string; whiteWordmark: string; darkPng: StaticImageData; whitePng: StaticImageData; } interface ContextMenuProps { logo: React.ReactNode; logoAssets: LogoAssets; } export default function LogoContextMenu({ logo, logoAssets, }: ContextMenuProps) { const [showMenu, setShowMenu] = useState<boolean>(false); const menuRef = useRef<HTMLDivElement>(null); const logoRef = useRef<HTMLDivElement>(null); const { theme } = useTheme(); const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); const rect = logoRef.current?.getBoundingClientRect(); if (rect) { setShowMenu(true); } }; const copySvgToClipboard = ( e: React.MouseEvent, svgContent: string, type: string, ) => { e.preventDefault(); e.stopPropagation(); navigator.clipboard .writeText(svgContent) .then(() => { toast.success("", { description: `${type} copied to clipboard`, }); }) .catch((err) => { toast.error("", { description: `Failed to copy ${type} to clipboard`, }); }); setShowMenu(false); }; const downloadPng = ( e: React.MouseEvent, pngData: StaticImageData, fileName: string, ) => { e.preventDefault(); e.stopPropagation(); const link = document.createElement("a"); link.href = pngData.src; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success(`Downloading the asset...`); setShowMenu(false); }; const downloadAllAssets = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const link = document.createElement("a"); link.href = "/branding/better-auth-brand-assets.zip"; link.download = "better-auth-branding-assets.zip"; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success("Downloading all assets..."); setShowMenu(false); }; useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setShowMenu(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const getAsset = <T,>(darkAsset: T, lightAsset: T): T => { return theme === "dark" ? darkAsset : lightAsset; }; return ( <div className="relative"> <div ref={logoRef} onContextMenu={handleContextMenu} className="cursor-pointer" > {logo} </div> {showMenu && ( <div ref={menuRef} className="fixed mx-10 z-50 bg-white dark:bg-black border border-gray-200 dark:border-border p-1 rounded-sm shadow-xl w-56 overflow-hidden animate-fd-dialog-in duration-500" > <div className=""> <div className="flex p-0 gap-1 flex-col text-xs"> <button onClick={(e) => copySvgToClipboard( e, getAsset(logoAssets.darkSvg, logoAssets.whiteSvg), "Logo SVG", ) } className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer" > <div className="flex items-center"> <span className="text-gray-400 dark:text-zinc-400/30">[</span> <Code className="h-[13.8px] w-[13.8px] mx-[3px]" /> <span className="text-gray-400 dark:text-zinc-400/30">]</span> </div> <span>Copy Logo as SVG </span> </button> <hr className="border-border/[60%]" /> <button onClick={(e) => copySvgToClipboard( e, getAsset(logoAssets.darkWordmark, logoAssets.whiteWordmark), "Logo Wordmark", ) } className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer" > <div className="flex items-center"> <span className="text-gray-400 dark:text-zinc-400/30">[</span> <Type className="h-[13.8px] w-[13.8px] mx-[3px]" /> <span className="text-gray-400 dark:text-zinc-400/30">]</span> </div> <span>Copy Logo as Wordmark </span> </button> <hr className="border-border/[60%]" /> <button onClick={(e) => downloadPng( e, getAsset(logoAssets.darkPng, logoAssets.whitePng), `better-auth-logo-${theme}.png`, ) } className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer" > <div className="flex items-center"> <span className="text-gray-400 dark:text-zinc-400/30">[</span> <Image className="h-[13.8px] w-[13.8px] mx-[3px]" /> <span className="text-gray-400 dark:text-zinc-400/30">]</span> </div> <span>Download Logo PNG</span> </button> <hr className="borde-border" /> <button onClick={(e) => downloadAllAssets(e)} className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer" > <div className="flex items-center"> <span className="text-gray-400 dark:text-zinc-400/30">[</span> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" className="h-[13.8px] w-[13.8px] mx-[3px]" > <path fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 8v8.8c0 1.12 0 1.68.218 2.108a2 2 0 0 0 .874.874c.427.218.987.218 2.105.218h9.606c1.118 0 1.677 0 2.104-.218c.377-.192.683-.498.875-.874c.218-.428.218-.987.218-2.105V8M4 8h16M4 8l1.365-2.39c.335-.585.503-.878.738-1.092c.209-.189.456-.332.723-.42C7.13 4 7.466 4 8.143 4h7.714c.676 0 1.015 0 1.318.099c.267.087.513.23.721.42c.236.213.404.506.74 1.093L20 8m-8 3v6m0 0l3-2m-3 2l-3-2" ></path> </svg> <span className="text-gray-400 dark:text-zinc-400/30">]</span> </div> <span>Brand Assets</span> </button> </div> </div> </div> )} </div> ); } ``` -------------------------------------------------------------------------------- /docs/app/docs/[[...slug]]/page.client.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useState, useTransition } from "react"; import { Check, Copy, ChevronDown, ExternalLink, MessageCircle, } from "lucide-react"; import { cn } from "@/lib/utils"; import { buttonVariants } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "fumadocs-ui/components/ui/popover"; import { cva } from "class-variance-authority"; import { type MouseEventHandler, useEffect, useRef } from "react"; import { useEffectEvent } from "fumadocs-core/utils/use-effect-event"; export function useCopyButton( onCopy: () => void | Promise<void>, ): [checked: boolean, onClick: MouseEventHandler] { const [checked, setChecked] = useState(false); const timeoutRef = useRef<number | null>(null); const onClick: MouseEventHandler = useEffectEvent(() => { if (timeoutRef.current) window.clearTimeout(timeoutRef.current); const res = Promise.resolve(onCopy()); void res.then(() => { setChecked(true); timeoutRef.current = window.setTimeout(() => { setChecked(false); }, 1500); }); }); // Avoid updates after being unmounted useEffect(() => { return () => { if (timeoutRef.current) window.clearTimeout(timeoutRef.current); }; }, []); return [checked, onClick]; } const cache = new Map<string, string>(); export function LLMCopyButton() { const [isLoading, startTransition] = useTransition(); const [checked, onClick] = useCopyButton(async () => { startTransition(async () => { const url = window.location.pathname + ".mdx"; const cached = cache.get(url); if (cached) { await navigator.clipboard.writeText(cached); } else { await navigator.clipboard.write([ new ClipboardItem({ "text/plain": fetch(url).then(async (res) => { const content = await res.text(); cache.set(url, content); return content; }), }), ]); } }); }); return ( <button disabled={isLoading} className={cn( buttonVariants({ variant: "secondary", size: "sm", className: "gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground", }), )} onClick={onClick} > {checked ? <Check /> : <Copy />} Copy Markdown </button> ); } const optionVariants = cva( "text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4", ); export function ViewOptions(props: { markdownUrl: string; githubUrl: string }) { const markdownUrl = new URL(props.markdownUrl, "https://better-auth.com"); const q = `Read ${markdownUrl}, I want to ask questions about it.`; const claude = `https://claude.ai/new?${new URLSearchParams({ q, })}`; const gpt = `https://chatgpt.com/?${new URLSearchParams({ hints: "search", q, })}`; const t3 = `https://t3.chat/new?${new URLSearchParams({ q, })}`; return ( <Popover> <PopoverTrigger className={cn( buttonVariants({ variant: "secondary", size: "sm", className: "gap-2", }), )} > Open in <ChevronDown className="size-3.5 text-fd-muted-foreground" /> </PopoverTrigger> <PopoverContent className="flex flex-col overflow-auto"> {[ { title: "Open in GitHub", href: props.githubUrl, icon: ( <svg fill="currentColor" role="img" viewBox="0 0 24 24"> <title>GitHub</title> <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" /> </svg> ), }, { title: "Open in ChatGPT", href: gpt, icon: ( <svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" > <title>OpenAI</title> <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" /> </svg> ), }, { title: "Open in Claude", href: claude, icon: ( <svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" > <title>Anthropic</title> <path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" /> </svg> ), }, { title: "Open in T3 Chat", href: t3, icon: <MessageCircle />, }, ].map((item) => ( <a key={item.href} href={item.href} rel="noreferrer noopener" target="_blank" className={cn(optionVariants())} > {item.icon} {item.title} <ExternalLink className="text-fd-muted-foreground size-3.5 ms-auto" /> </a> ))} </PopoverContent> </Popover> ); } ``` -------------------------------------------------------------------------------- /packages/cli/src/commands/mcp.ts: -------------------------------------------------------------------------------- ```typescript import { Command } from "commander"; import { execSync } from "child_process"; import * as os from "os"; import * as fs from "fs"; import * as path from "path"; import chalk from "chalk"; import { base64 } from "@better-auth/utils/base64"; interface MCPOptions { cursor?: boolean; claudeCode?: boolean; openCode?: boolean; manual?: boolean; } export async function mcpAction(options: MCPOptions) { const mcpUrl = "https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp"; const mcpName = "Better Auth"; if (options.cursor) { await handleCursorAction(mcpUrl, mcpName); } else if (options.claudeCode) { handleClaudeCodeAction(mcpUrl); } else if (options.openCode) { handleOpenCodeAction(mcpUrl); } else if (options.manual) { handleManualAction(mcpUrl, mcpName); } else { showAllOptions(mcpUrl, mcpName); } } async function handleCursorAction(mcpUrl: string, mcpName: string) { const mcpConfig = { url: mcpUrl, }; const encodedConfig = base64.encode( new TextEncoder().encode(JSON.stringify(mcpConfig)), ); const deeplinkUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(mcpName)}&config=${encodedConfig}`; console.log(chalk.bold.blue("🚀 Adding Better Auth MCP to Cursor...")); try { const platform = os.platform(); let command: string; switch (platform) { case "darwin": command = `open "${deeplinkUrl}"`; break; case "win32": command = `start "" "${deeplinkUrl}"`; break; case "linux": command = `xdg-open "${deeplinkUrl}"`; break; default: throw new Error(`Unsupported platform: ${platform}`); } execSync(command, { stdio: "inherit" }); console.log(chalk.green("\n✓ Cursor MCP installed successfully!")); } catch (error) { console.log( chalk.yellow( "\n⚠ Could not automatically open Cursor. Please copy the deeplink URL above and open it manually.", ), ); console.log( chalk.gray( "\nYou can also manually add this configuration to your Cursor MCP settings:", ), ); console.log(chalk.gray(JSON.stringify(mcpConfig, null, 2))); } console.log(chalk.bold.white("\n✨ Next Steps:")); console.log( chalk.gray("• The MCP server will be added to your Cursor configuration"), ); console.log( chalk.gray("• You can now use Better Auth features directly in Cursor"), ); } function handleClaudeCodeAction(mcpUrl: string) { console.log(chalk.bold.blue("🤖 Adding Better Auth MCP to Claude Code...")); const command = `claude mcp add --transport http better-auth ${mcpUrl}`; try { execSync(command, { stdio: "inherit" }); console.log(chalk.green("\n✓ Claude Code MCP installed successfully!")); } catch (error) { console.log( chalk.yellow( "\n⚠ Could not automatically add to Claude Code. Please run this command manually:", ), ); console.log(chalk.cyan(command)); } console.log(chalk.bold.white("\n✨ Next Steps:")); console.log( chalk.gray( "• The MCP server will be added to your Claude Code configuration", ), ); console.log( chalk.gray( "• You can now use Better Auth features directly in Claude Code", ), ); } function handleOpenCodeAction(mcpUrl: string) { console.log(chalk.bold.blue("🔧 Adding Better Auth MCP to Open Code...")); const openCodeConfig = { $schema: "https://opencode.ai/config.json", mcp: { "Better Auth": { type: "remote", url: mcpUrl, enabled: true, }, }, }; const configPath = path.join(process.cwd(), "opencode.json"); try { let existingConfig: { mcp?: Record<string, unknown>; [key: string]: unknown; } = {}; if (fs.existsSync(configPath)) { const existingContent = fs.readFileSync(configPath, "utf8"); existingConfig = JSON.parse(existingContent); } const mergedConfig = { ...existingConfig, ...openCodeConfig, mcp: { ...existingConfig.mcp, ...openCodeConfig.mcp, }, }; fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2)); console.log( chalk.green(`\n✓ Open Code configuration written to ${configPath}`), ); console.log(chalk.green("✓ Better Auth MCP added successfully!")); } catch (error) { console.log( chalk.yellow( "\n⚠ Could not automatically write opencode.json. Please add this configuration manually:", ), ); console.log(chalk.cyan(JSON.stringify(openCodeConfig, null, 2))); } console.log(chalk.bold.white("\n✨ Next Steps:")); console.log(chalk.gray("• Restart Open Code to load the new MCP server")); console.log( chalk.gray("• You can now use Better Auth features directly in Open Code"), ); } function handleManualAction(mcpUrl: string, mcpName: string) { console.log(chalk.bold.blue("📝 Adding Better Auth MCP Configuration...")); const manualConfig = { [mcpName]: { url: mcpUrl, }, }; const configPath = path.join(process.cwd(), "mcp.json"); try { let existingConfig = {}; if (fs.existsSync(configPath)) { const existingContent = fs.readFileSync(configPath, "utf8"); existingConfig = JSON.parse(existingContent); } const mergedConfig = { ...existingConfig, ...manualConfig, }; fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2)); console.log(chalk.green(`\n✓ MCP configuration written to ${configPath}`)); console.log(chalk.green("✓ Better Auth MCP added successfully!")); } catch (error) { console.log( chalk.yellow( "\n⚠ Could not automatically write mcp.json. Please add this configuration manually:", ), ); console.log(chalk.cyan(JSON.stringify(manualConfig, null, 2))); } console.log(chalk.bold.white("\n✨ Next Steps:")); console.log(chalk.gray("• Restart your MCP client to load the new server")); console.log( chalk.gray( "• You can now use Better Auth features directly in your MCP client", ), ); } function showAllOptions(mcpUrl: string, mcpName: string) { console.log(chalk.bold.blue("🔌 Better Auth MCP Server")); console.log(chalk.gray("Choose your MCP client to get started:")); console.log(); console.log(chalk.bold.white("Available Commands:")); console.log(chalk.cyan(" --cursor ") + chalk.gray("Add to Cursor")); console.log( chalk.cyan(" --claude-code ") + chalk.gray("Add to Claude Code"), ); console.log(chalk.cyan(" --open-code ") + chalk.gray("Add to Open Code")); console.log( chalk.cyan(" --manual ") + chalk.gray("Manual configuration"), ); console.log(); } export const mcp = new Command("mcp") .description("Add Better Auth MCP server to MCP Clients") .option("--cursor", "Automatically open Cursor with the MCP configuration") .option("--claude-code", "Show Claude Code MCP configuration command") .option("--open-code", "Show Open Code MCP configuration") .option("--manual", "Show manual MCP configuration for mcp.json") .action(mcpAction); ``` -------------------------------------------------------------------------------- /packages/core/src/types/context.ts: -------------------------------------------------------------------------------- ```typescript import type { Account, BetterAuthDBSchema, SecondaryStorage, Session, User, Verification, } from "../db"; import type { OAuthProvider } from "../oauth2"; import { createLogger } from "../env"; import type { DBAdapter, Where } from "../db/adapter"; import type { BetterAuthCookies } from "./cookie"; import type { DBPreservedModels } from "../db"; import type { LiteralUnion } from "./helper"; import type { CookieOptions, EndpointContext } from "better-call"; import type { BetterAuthOptions, BetterAuthRateLimitOptions, } from "./init-options"; export type GenericEndpointContext< Options extends BetterAuthOptions = BetterAuthOptions, > = EndpointContext<string, any> & { context: AuthContext<Options>; }; export interface InternalAdapter< Options extends BetterAuthOptions = BetterAuthOptions, > { createOAuthUser( user: Omit<User, "id" | "createdAt" | "updatedAt">, account: Omit<Account, "userId" | "id" | "createdAt" | "updatedAt"> & Partial<Account>, ): Promise<{ user: User; account: Account }>; createUser<T extends Record<string, any>>( user: Omit<User, "id" | "createdAt" | "updatedAt" | "emailVerified"> & Partial<User> & Record<string, any>, ): Promise<T & User>; createAccount<T extends Record<string, any>>( account: Omit<Account, "id" | "createdAt" | "updatedAt"> & Partial<Account> & T, ): Promise<T & Account>; listSessions(userId: string): Promise<Session[]>; listUsers( limit?: number, offset?: number, sortBy?: { field: string; direction: "asc" | "desc" }, where?: Where[], ): Promise<User[]>; countTotalUsers(where?: Where[]): Promise<number>; deleteUser(userId: string): Promise<void>; createSession( userId: string, dontRememberMe?: boolean, override?: Partial<Session> & Record<string, any>, overrideAll?: boolean, ): Promise<Session>; findSession(token: string): Promise<{ session: Session & Record<string, any>; user: User & Record<string, any>; } | null>; findSessions( sessionTokens: string[], ): Promise<{ session: Session; user: User }[]>; updateSession( sessionToken: string, session: Partial<Session> & Record<string, any>, ): Promise<Session | null>; deleteSession(token: string): Promise<void>; deleteAccounts(userId: string): Promise<void>; deleteAccount(accountId: string): Promise<void>; deleteSessions(userIdOrSessionTokens: string | string[]): Promise<void>; findOAuthUser( email: string, accountId: string, providerId: string, ): Promise<{ user: User; accounts: Account[] } | null>; findUserByEmail( email: string, options?: { includeAccounts: boolean }, ): Promise<{ user: User; accounts: Account[] } | null>; findUserById(userId: string): Promise<User | null>; linkAccount( account: Omit<Account, "id" | "createdAt" | "updatedAt"> & Partial<Account>, ): Promise<Account>; // fixme: any type updateUser( userId: string, data: Partial<User> & Record<string, any>, ): Promise<any>; updateUserByEmail( email: string, data: Partial<User & Record<string, any>>, ): Promise<User>; updatePassword(userId: string, password: string): Promise<void>; findAccounts(userId: string): Promise<Account[]>; findAccount(accountId: string): Promise<Account | null>; findAccountByProviderId( accountId: string, providerId: string, ): Promise<Account | null>; findAccountByUserId(userId: string): Promise<Account[]>; updateAccount(id: string, data: Partial<Account>): Promise<Account>; createVerificationValue( data: Omit<Verification, "createdAt" | "id" | "updatedAt"> & Partial<Verification>, ): Promise<Verification>; findVerificationValue(identifier: string): Promise<Verification | null>; deleteVerificationValue(id: string): Promise<void>; deleteVerificationByIdentifier(identifier: string): Promise<void>; updateVerificationValue( id: string, data: Partial<Verification>, ): Promise<Verification>; } type CreateCookieGetterFn = ( cookieName: string, overrideAttributes?: Partial<CookieOptions>, ) => { name: string; attributes: CookieOptions; }; type CheckPasswordFn<Options extends BetterAuthOptions = BetterAuthOptions> = ( userId: string, ctx: GenericEndpointContext<Options>, ) => Promise<boolean>; export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> = { options: Options; appName: string; baseURL: string; trustedOrigins: string[]; oauthConfig?: { /** * This is dangerous and should only be used in dev or staging environments. */ skipStateCookieCheck?: boolean; }; /** * New session that will be set after the request * meaning: there is a `set-cookie` header that will set * the session cookie. This is the fetched session. And it's set * by `setNewSession` method. */ newSession: { session: Session & Record<string, any>; user: User & Record<string, any>; } | null; session: { session: Session & Record<string, any>; user: User & Record<string, any>; } | null; setNewSession: ( session: { session: Session & Record<string, any>; user: User & Record<string, any>; } | null, ) => void; socialProviders: OAuthProvider[]; authCookies: BetterAuthCookies; logger: ReturnType<typeof createLogger>; rateLimit: { enabled: boolean; window: number; max: number; storage: "memory" | "database" | "secondary-storage"; } & BetterAuthRateLimitOptions; adapter: DBAdapter<Options>; internalAdapter: InternalAdapter<Options>; createAuthCookie: CreateCookieGetterFn; secret: string; sessionConfig: { updateAge: number; expiresIn: number; freshAge: number; }; generateId: (options: { model: LiteralUnion<DBPreservedModels, string>; size?: number; }) => string | false; secondaryStorage: SecondaryStorage | undefined; password: { hash: (password: string) => Promise<string>; verify: (data: { password: string; hash: string }) => Promise<boolean>; config: { minPasswordLength: number; maxPasswordLength: number; }; checkPassword: CheckPasswordFn<Options>; }; tables: BetterAuthDBSchema; runMigrations: () => Promise<void>; publishTelemetry: (event: { type: string; anonymousId?: string; payload: Record<string, any>; }) => Promise<void>; /** * This skips the origin check for all requests. * * set to true by default for `test` environments and `false` * for other environments. * * It's inferred from the `options.advanced?.disableCSRFCheck` * option or `options.advanced?.disableOriginCheck` option. * * @default false */ skipOriginCheck: boolean; /** * This skips the CSRF check for all requests. * * This is inferred from the `options.advanced?. * disableCSRFCheck` option. * * @default false */ skipCSRFCheck: boolean; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/callback.ts: -------------------------------------------------------------------------------- ```typescript import * as z from "zod"; import { setSessionCookie } from "../../cookies"; import { setTokenUtil } from "../../oauth2/utils"; import { handleOAuthUserInfo } from "../../oauth2/link-account"; import { parseState } from "../../oauth2/state"; import { HIDE_METADATA } from "../../utils/hide-metadata"; import { createAuthEndpoint } from "@better-auth/core/api"; import { safeJSONParse } from "../../utils/json"; import type { OAuth2Tokens } from "@better-auth/core/oauth2"; const schema = z.object({ code: z.string().optional(), error: z.string().optional(), device_id: z.string().optional(), error_description: z.string().optional(), state: z.string().optional(), user: z.string().optional(), }); export const callbackOAuth = createAuthEndpoint( "/callback/:id", { method: ["GET", "POST"], body: schema.optional(), query: schema.optional(), metadata: HIDE_METADATA, }, async (c) => { let queryOrBody: z.infer<typeof schema>; const defaultErrorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; try { if (c.method === "GET") { queryOrBody = schema.parse(c.query); } else if (c.method === "POST") { queryOrBody = schema.parse(c.body); } else { throw new Error("Unsupported method"); } } catch (e) { c.context.logger.error("INVALID_CALLBACK_REQUEST", e); throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`); } const { code, error, state, error_description, device_id } = queryOrBody; if (!state) { c.context.logger.error("State not found", error); const sep = defaultErrorURL.includes("?") ? "&" : "?"; const url = `${defaultErrorURL}${sep}state=state_not_found`; throw c.redirect(url); } const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp, } = await parseState(c); function redirectOnError(error: string, description?: string) { const baseURL = errorURL ?? defaultErrorURL; const params = new URLSearchParams({ error }); if (description) params.set("error_description", description); const sep = baseURL.includes("?") ? "&" : "?"; const url = `${baseURL}${sep}${params.toString()}`; throw c.redirect(url); } if (error) { redirectOnError(error, error_description); } if (!code) { c.context.logger.error("Code not found"); throw redirectOnError("no_code"); } const provider = c.context.socialProviders.find( (p) => p.id === c.params.id, ); if (!provider) { c.context.logger.error( "Oauth provider with id", c.params.id, "not found", ); throw redirectOnError("oauth_provider_not_found"); } let tokens: OAuth2Tokens; try { tokens = await provider.validateAuthorizationCode({ code: code, codeVerifier, deviceId: device_id, redirectURI: `${c.context.baseURL}/callback/${provider.id}`, }); } catch (e) { c.context.logger.error("", e); throw redirectOnError("invalid_code"); } const userInfo = await provider .getUserInfo({ ...tokens, user: c.body?.user ? safeJSONParse<any>(c.body.user) : undefined, }) .then((res) => res?.user); if (!userInfo) { c.context.logger.error("Unable to get user info"); return redirectOnError("unable_to_get_user_info"); } if (!callbackURL) { c.context.logger.error("No callback URL found"); throw redirectOnError("no_callback_url"); } if (link) { const trustedProviders = c.context.options.account?.accountLinking?.trustedProviders; const isTrustedProvider = trustedProviders?.includes( provider.id as "apple", ); if ( (!isTrustedProvider && !userInfo.emailVerified) || c.context.options.account?.accountLinking?.enabled === false ) { c.context.logger.error("Unable to link account - untrusted provider"); return redirectOnError("unable_to_link_account"); } if ( userInfo.email !== link.email && c.context.options.account?.accountLinking?.allowDifferentEmails !== true ) { return redirectOnError("email_doesn't_match"); } const existingAccount = await c.context.internalAdapter.findAccount( String(userInfo.id), ); if (existingAccount) { if (existingAccount.userId.toString() !== link.userId.toString()) { return redirectOnError("account_already_linked_to_different_user"); } const updateData = Object.fromEntries( Object.entries({ accessToken: await setTokenUtil(tokens.accessToken, c.context), refreshToken: await setTokenUtil(tokens.refreshToken, c.context), idToken: tokens.idToken, accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshTokenExpiresAt: tokens.refreshTokenExpiresAt, scope: tokens.scopes?.join(","), }).filter(([_, value]) => value !== undefined), ); await c.context.internalAdapter.updateAccount( existingAccount.id, updateData, ); } else { const newAccount = await c.context.internalAdapter.createAccount({ userId: link.userId, providerId: provider.id, accountId: String(userInfo.id), ...tokens, accessToken: await setTokenUtil(tokens.accessToken, c.context), refreshToken: await setTokenUtil(tokens.refreshToken, c.context), scope: tokens.scopes?.join(","), }); if (!newAccount) { return redirectOnError("unable_to_link_account"); } } let toRedirectTo: string; try { const url = callbackURL; toRedirectTo = url.toString(); } catch { toRedirectTo = callbackURL; } throw c.redirect(toRedirectTo); } if (!userInfo.email) { c.context.logger.error( "Provider did not return email. This could be due to misconfiguration in the provider settings.", ); return redirectOnError("email_not_found"); } const result = await handleOAuthUserInfo(c, { userInfo: { ...userInfo, id: String(userInfo.id), email: userInfo.email, name: userInfo.name || userInfo.email, }, account: { providerId: provider.id, accountId: String(userInfo.id), ...tokens, scope: tokens.scopes?.join(","), }, callbackURL, disableSignUp: (provider.disableImplicitSignUp && !requestSignUp) || provider.options?.disableSignUp, overrideUserInfo: provider.options?.overrideUserInfoOnSignIn, }); if (result.error) { c.context.logger.error(result.error.split(" ").join("_")); return redirectOnError(result.error.split(" ").join("_")); } const { session, user } = result.data!; await setSessionCookie(c, { session, user, }); let toRedirectTo: string; try { const url = result.isRegister ? newUserURL || callbackURL : callbackURL; toRedirectTo = url.toString(); } catch { toRedirectTo = result.isRegister ? newUserURL || callbackURL : callbackURL; } throw c.redirect(toRedirectTo); }, ); ```