This is page 15 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── 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 -------------------------------------------------------------------------------- /demo/nextjs/components/ui/command.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Command as CommandPrimitive } from "cmdk"; 5 | import { SearchIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogHeader, 13 | DialogTitle, 14 | } from "@/components/ui/dialog"; 15 | 16 | function Command({ 17 | className, 18 | ...props 19 | }: React.ComponentProps<typeof CommandPrimitive>) { 20 | return ( 21 | <CommandPrimitive 22 | data-slot="command" 23 | className={cn( 24 | "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", 25 | className, 26 | )} 27 | {...props} 28 | /> 29 | ); 30 | } 31 | 32 | function CommandDialog({ 33 | title = "Command Palette", 34 | description = "Search for a command to run...", 35 | children, 36 | ...props 37 | }: React.ComponentProps<typeof Dialog> & { 38 | title?: string; 39 | description?: string; 40 | }) { 41 | return ( 42 | <Dialog {...props}> 43 | <DialogHeader className="sr-only"> 44 | <DialogTitle>{title}</DialogTitle> 45 | <DialogDescription>{description}</DialogDescription> 46 | </DialogHeader> 47 | <DialogContent className="overflow-hidden p-0"> 48 | <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> 49 | {children} 50 | </Command> 51 | </DialogContent> 52 | </Dialog> 53 | ); 54 | } 55 | 56 | function CommandInput({ 57 | className, 58 | ...props 59 | }: React.ComponentProps<typeof CommandPrimitive.Input>) { 60 | return ( 61 | <div 62 | data-slot="command-input-wrapper" 63 | className="flex h-9 items-center gap-2 border-b px-3" 64 | > 65 | <SearchIcon className="size-4 shrink-0 opacity-50" /> 66 | <CommandPrimitive.Input 67 | data-slot="command-input" 68 | className={cn( 69 | "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", 70 | className, 71 | )} 72 | {...props} 73 | /> 74 | </div> 75 | ); 76 | } 77 | 78 | function CommandList({ 79 | className, 80 | ...props 81 | }: React.ComponentProps<typeof CommandPrimitive.List>) { 82 | return ( 83 | <CommandPrimitive.List 84 | data-slot="command-list" 85 | className={cn( 86 | "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", 87 | className, 88 | )} 89 | {...props} 90 | /> 91 | ); 92 | } 93 | 94 | function CommandEmpty({ 95 | ...props 96 | }: React.ComponentProps<typeof CommandPrimitive.Empty>) { 97 | return ( 98 | <CommandPrimitive.Empty 99 | data-slot="command-empty" 100 | className="py-6 text-center text-sm" 101 | {...props} 102 | /> 103 | ); 104 | } 105 | 106 | function CommandGroup({ 107 | className, 108 | ...props 109 | }: React.ComponentProps<typeof CommandPrimitive.Group>) { 110 | return ( 111 | <CommandPrimitive.Group 112 | data-slot="command-group" 113 | className={cn( 114 | "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", 115 | className, 116 | )} 117 | {...props} 118 | /> 119 | ); 120 | } 121 | 122 | function CommandSeparator({ 123 | className, 124 | ...props 125 | }: React.ComponentProps<typeof CommandPrimitive.Separator>) { 126 | return ( 127 | <CommandPrimitive.Separator 128 | data-slot="command-separator" 129 | className={cn("bg-border -mx-1 h-px", className)} 130 | {...props} 131 | /> 132 | ); 133 | } 134 | 135 | function CommandItem({ 136 | className, 137 | ...props 138 | }: React.ComponentProps<typeof CommandPrimitive.Item>) { 139 | return ( 140 | <CommandPrimitive.Item 141 | data-slot="command-item" 142 | className={cn( 143 | "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 144 | className, 145 | )} 146 | {...props} 147 | /> 148 | ); 149 | } 150 | 151 | function CommandShortcut({ 152 | className, 153 | ...props 154 | }: React.ComponentProps<"span">) { 155 | return ( 156 | <span 157 | data-slot="command-shortcut" 158 | className={cn( 159 | "text-muted-foreground ml-auto text-xs tracking-widest", 160 | className, 161 | )} 162 | {...props} 163 | /> 164 | ); 165 | } 166 | 167 | export { 168 | Command, 169 | CommandDialog, 170 | CommandInput, 171 | CommandList, 172 | CommandEmpty, 173 | CommandGroup, 174 | CommandItem, 175 | CommandShortcut, 176 | CommandSeparator, 177 | }; 178 | ``` -------------------------------------------------------------------------------- /docs/components/ui/command.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Command as CommandPrimitive } from "cmdk"; 5 | import { SearchIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogHeader, 13 | DialogTitle, 14 | } from "@/components/ui/dialog"; 15 | 16 | function Command({ 17 | className, 18 | ...props 19 | }: React.ComponentProps<typeof CommandPrimitive>) { 20 | return ( 21 | <CommandPrimitive 22 | data-slot="command" 23 | className={cn( 24 | "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", 25 | className, 26 | )} 27 | {...props} 28 | /> 29 | ); 30 | } 31 | 32 | function CommandDialog({ 33 | title = "Command Palette", 34 | description = "Search for a command to run...", 35 | children, 36 | ...props 37 | }: React.ComponentProps<typeof Dialog> & { 38 | title?: string; 39 | description?: string; 40 | }) { 41 | return ( 42 | <Dialog {...props}> 43 | <DialogHeader className="sr-only"> 44 | <DialogTitle>{title}</DialogTitle> 45 | <DialogDescription>{description}</DialogDescription> 46 | </DialogHeader> 47 | <DialogContent className="overflow-hidden p-0"> 48 | <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> 49 | {children} 50 | </Command> 51 | </DialogContent> 52 | </Dialog> 53 | ); 54 | } 55 | 56 | function CommandInput({ 57 | className, 58 | ...props 59 | }: React.ComponentProps<typeof CommandPrimitive.Input>) { 60 | return ( 61 | <div 62 | data-slot="command-input-wrapper" 63 | className="flex h-9 items-center gap-2 border-b px-3" 64 | > 65 | <SearchIcon className="size-4 shrink-0 opacity-50" /> 66 | <CommandPrimitive.Input 67 | data-slot="command-input" 68 | className={cn( 69 | "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", 70 | className, 71 | )} 72 | {...props} 73 | /> 74 | </div> 75 | ); 76 | } 77 | 78 | function CommandList({ 79 | className, 80 | ...props 81 | }: React.ComponentProps<typeof CommandPrimitive.List>) { 82 | return ( 83 | <CommandPrimitive.List 84 | data-slot="command-list" 85 | className={cn( 86 | "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", 87 | className, 88 | )} 89 | {...props} 90 | /> 91 | ); 92 | } 93 | 94 | function CommandEmpty({ 95 | ...props 96 | }: React.ComponentProps<typeof CommandPrimitive.Empty>) { 97 | return ( 98 | <CommandPrimitive.Empty 99 | data-slot="command-empty" 100 | className="py-6 text-center text-sm" 101 | {...props} 102 | /> 103 | ); 104 | } 105 | 106 | function CommandGroup({ 107 | className, 108 | ...props 109 | }: React.ComponentProps<typeof CommandPrimitive.Group>) { 110 | return ( 111 | <CommandPrimitive.Group 112 | data-slot="command-group" 113 | className={cn( 114 | "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", 115 | className, 116 | )} 117 | {...props} 118 | /> 119 | ); 120 | } 121 | 122 | function CommandSeparator({ 123 | className, 124 | ...props 125 | }: React.ComponentProps<typeof CommandPrimitive.Separator>) { 126 | return ( 127 | <CommandPrimitive.Separator 128 | data-slot="command-separator" 129 | className={cn("bg-border -mx-1 h-px", className)} 130 | {...props} 131 | /> 132 | ); 133 | } 134 | 135 | function CommandItem({ 136 | className, 137 | ...props 138 | }: React.ComponentProps<typeof CommandPrimitive.Item>) { 139 | return ( 140 | <CommandPrimitive.Item 141 | data-slot="command-item" 142 | className={cn( 143 | "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 144 | className, 145 | )} 146 | {...props} 147 | /> 148 | ); 149 | } 150 | 151 | function CommandShortcut({ 152 | className, 153 | ...props 154 | }: React.ComponentProps<"span">) { 155 | return ( 156 | <span 157 | data-slot="command-shortcut" 158 | className={cn( 159 | "text-muted-foreground ml-auto text-xs tracking-widest", 160 | className, 161 | )} 162 | {...props} 163 | /> 164 | ); 165 | } 166 | 167 | export { 168 | Command, 169 | CommandDialog, 170 | CommandInput, 171 | CommandList, 172 | CommandEmpty, 173 | CommandGroup, 174 | CommandItem, 175 | CommandShortcut, 176 | CommandSeparator, 177 | }; 178 | ``` -------------------------------------------------------------------------------- /docs/components/message-feedback.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Check, Copy, ThumbsDown, ThumbsUp } from "lucide-react"; 5 | import { cn } from "@/lib/utils"; 6 | import { buttonVariants } from "fumadocs-ui/components/ui/button"; 7 | import { 8 | submitFeedbackToInkeep, 9 | logEventToInkeep, 10 | } from "@/lib/inkeep-analytics"; 11 | 12 | interface MessageFeedbackProps { 13 | messageId: string; 14 | userMessageId?: string; 15 | content: string; 16 | className?: string; 17 | } 18 | 19 | export function MessageFeedback({ 20 | messageId, 21 | userMessageId, 22 | content, 23 | className, 24 | }: MessageFeedbackProps) { 25 | const [feedback, setFeedback] = useState<"positive" | "negative" | null>( 26 | null, 27 | ); 28 | const [copied, setCopied] = useState(false); 29 | const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false); 30 | const [showSuccessCheckmark, setShowSuccessCheckmark] = useState< 31 | "positive" | "negative" | null 32 | >(null); 33 | 34 | const handleFeedback = async (type: "positive" | "negative") => { 35 | if (isSubmittingFeedback || feedback === type) return; 36 | 37 | const feedbackMessageId = userMessageId || messageId; 38 | 39 | setIsSubmittingFeedback(true); 40 | 41 | try { 42 | await submitFeedbackToInkeep(feedbackMessageId, type, [ 43 | { 44 | label: 45 | type === "positive" ? "helpful_response" : "unhelpful_response", 46 | details: 47 | type === "positive" 48 | ? "The response was helpful" 49 | : "The response was not helpful", 50 | }, 51 | ]); 52 | 53 | setFeedback(type); 54 | setShowSuccessCheckmark(type); 55 | 56 | setTimeout(() => { 57 | setShowSuccessCheckmark(null); 58 | }, 1000); 59 | } catch (error) { 60 | } finally { 61 | setIsSubmittingFeedback(false); 62 | } 63 | }; 64 | 65 | const handleCopy = async () => { 66 | if (copied) return; 67 | 68 | const eventMessageId = userMessageId || messageId; 69 | 70 | try { 71 | await navigator.clipboard.writeText(content); 72 | setCopied(true); 73 | 74 | await logEventToInkeep("message:copied", "message", eventMessageId); 75 | 76 | setTimeout(() => setCopied(false), 2000); 77 | } catch (error) { 78 | // Silently handle error 79 | } 80 | }; 81 | 82 | return ( 83 | <div 84 | className={cn( 85 | "flex items-center gap-1 mt-3 pt-2 border-t border-fd-border/30", 86 | className, 87 | )} 88 | > 89 | <button 90 | type="button" 91 | onClick={() => handleFeedback("positive")} 92 | disabled={isSubmittingFeedback} 93 | className={cn( 94 | buttonVariants({ 95 | size: "icon-sm", 96 | color: feedback === "positive" ? "primary" : "ghost", 97 | className: cn( 98 | "h-7 w-7 transition-colors", 99 | isSubmittingFeedback && "opacity-50 cursor-not-allowed", 100 | feedback === "positive" 101 | ? "bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50" 102 | : "hover:bg-fd-accent hover:text-fd-accent-foreground", 103 | ), 104 | }), 105 | )} 106 | title={ 107 | showSuccessCheckmark === "positive" 108 | ? "Feedback submitted!" 109 | : "Helpful" 110 | } 111 | > 112 | {showSuccessCheckmark === "positive" ? ( 113 | <Check className="h-3.5 w-3.5 text-green-600 animate-in fade-in duration-200" /> 114 | ) : ( 115 | <ThumbsUp className="h-3.5 w-3.5 transition-all duration-200" /> 116 | )} 117 | </button> 118 | 119 | <button 120 | type="button" 121 | onClick={() => handleFeedback("negative")} 122 | disabled={isSubmittingFeedback} 123 | className={cn( 124 | buttonVariants({ 125 | size: "icon-sm", 126 | color: feedback === "negative" ? "primary" : "ghost", 127 | className: cn( 128 | "h-7 w-7 transition-colors", 129 | isSubmittingFeedback && "opacity-50 cursor-not-allowed", 130 | feedback === "negative" 131 | ? "bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50" 132 | : "hover:bg-fd-accent hover:text-fd-accent-foreground", 133 | ), 134 | }), 135 | )} 136 | title={ 137 | showSuccessCheckmark === "negative" 138 | ? "Feedback submitted!" 139 | : "Not helpful" 140 | } 141 | > 142 | {showSuccessCheckmark === "negative" ? ( 143 | <Check className="h-3.5 w-3.5 text-green-600 animate-in fade-in duration-200" /> 144 | ) : ( 145 | <ThumbsDown className="h-3.5 w-3.5 transition-all duration-200" /> 146 | )} 147 | </button> 148 | 149 | <button 150 | type="button" 151 | onClick={handleCopy} 152 | className={cn( 153 | buttonVariants({ 154 | size: "icon-sm", 155 | color: "ghost", 156 | className: 157 | "h-7 w-7 hover:bg-fd-accent hover:text-fd-accent-foreground transition-colors", 158 | }), 159 | )} 160 | title={copied ? "Copied!" : "Copy message"} 161 | > 162 | {copied ? ( 163 | <Check className="h-3.5 w-3.5 text-green-600" /> 164 | ) : ( 165 | <Copy className="h-3.5 w-3.5" /> 166 | )} 167 | </button> 168 | </div> 169 | ); 170 | } 171 | ``` -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@better-auth/core", 3 | "version": "1.4.0-beta.12", 4 | "description": "The most comprehensive authentication library for TypeScript.", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "exports": { 9 | ".": { 10 | "import": { 11 | "types": "./dist/index.d.ts", 12 | "default": "./dist/index.js" 13 | }, 14 | "require": { 15 | "types": "./dist/index.d.cts", 16 | "default": "./dist/index.cjs" 17 | } 18 | }, 19 | "./api": { 20 | "import": { 21 | "types": "./dist/api/index.d.ts", 22 | "default": "./dist/api/index.js" 23 | }, 24 | "require": { 25 | "types": "./dist/api/index.d.cts", 26 | "default": "./dist/api/index.cjs" 27 | } 28 | }, 29 | "./async_hooks": { 30 | "import": { 31 | "types": "./dist/async_hooks/index.d.ts", 32 | "default": "./dist/async_hooks/index.js" 33 | }, 34 | "require": { 35 | "types": "./dist/async_hooks/index.d.cts", 36 | "default": "./dist/async_hooks/index.cjs" 37 | } 38 | }, 39 | "./context": { 40 | "import": { 41 | "types": "./dist/context/index.d.ts", 42 | "default": "./dist/context/index.js" 43 | }, 44 | "require": { 45 | "types": "./dist/context/index.d.cts", 46 | "default": "./dist/context/index.cjs" 47 | } 48 | }, 49 | "./env": { 50 | "import": { 51 | "types": "./dist/env/index.d.ts", 52 | "default": "./dist/env/index.js" 53 | }, 54 | "require": { 55 | "types": "./dist/env/index.d.cts", 56 | "default": "./dist/env/index.cjs" 57 | } 58 | }, 59 | "./error": { 60 | "import": { 61 | "types": "./dist/error/index.d.ts", 62 | "default": "./dist/error/index.js" 63 | }, 64 | "require": { 65 | "types": "./dist/error/index.d.cts", 66 | "default": "./dist/error/index.cjs" 67 | } 68 | }, 69 | "./utils": { 70 | "import": { 71 | "types": "./dist/utils/index.d.ts", 72 | "default": "./dist/utils/index.js" 73 | }, 74 | "require": { 75 | "types": "./dist/utils/index.d.cts", 76 | "default": "./dist/utils/index.cjs" 77 | } 78 | }, 79 | "./social-providers": { 80 | "import": { 81 | "types": "./dist/social-providers/index.d.ts", 82 | "default": "./dist/social-providers/index.js" 83 | }, 84 | "require": { 85 | "types": "./dist/social-providers/index.d.cts", 86 | "default": "./dist/social-providers/index.cjs" 87 | } 88 | }, 89 | "./db": { 90 | "import": { 91 | "types": "./dist/db/index.d.ts", 92 | "default": "./dist/db/index.js" 93 | }, 94 | "require": { 95 | "types": "./dist/db/index.d.cts", 96 | "default": "./dist/db/index.cjs" 97 | } 98 | }, 99 | "./db/adapter": { 100 | "import": { 101 | "types": "./dist/db/adapter/index.d.ts", 102 | "default": "./dist/db/adapter/index.js" 103 | }, 104 | "require": { 105 | "types": "./dist/db/adapter/index.d.cts", 106 | "default": "./dist/db/adapter/index.cjs" 107 | } 108 | }, 109 | "./oauth2": { 110 | "import": { 111 | "types": "./dist/oauth2/index.d.ts", 112 | "default": "./dist/oauth2/index.js" 113 | }, 114 | "require": { 115 | "types": "./dist/oauth2/index.d.cts", 116 | "default": "./dist/oauth2/index.cjs" 117 | } 118 | } 119 | }, 120 | "typesVersions": { 121 | "*": { 122 | "index": [ 123 | "dist/index.d.ts" 124 | ], 125 | "async_hooks": [ 126 | "dist/async_hooks.d.ts" 127 | ], 128 | "db": [ 129 | "dist/db.d.ts" 130 | ], 131 | "db/adapter": [ 132 | "dist/db/adapter/index.d.ts" 133 | ], 134 | "env": [ 135 | "dist/env.d.ts" 136 | ], 137 | "error": [ 138 | "dist/error.d.ts" 139 | ], 140 | "middleware": [ 141 | "dist/middleware.d.ts" 142 | ], 143 | "oauth2": [ 144 | "dist/oauth2.d.ts" 145 | ], 146 | "social-providers": [ 147 | "dist/social-providers.d.ts" 148 | ], 149 | "utils": [ 150 | "dist/utils.d.ts" 151 | ] 152 | } 153 | }, 154 | "scripts": { 155 | "build": "tsdown", 156 | "dev": "tsdown --watch", 157 | "typecheck": "tsc --project tsconfig.json" 158 | }, 159 | "devDependencies": { 160 | "@better-auth/utils": "0.3.0", 161 | "@better-fetch/fetch": "catalog:", 162 | "@types/better-sqlite3": "^7.6.13", 163 | "better-call": "catalog:", 164 | "better-sqlite3": "^12.4.1", 165 | "jose": "^6.1.0", 166 | "kysely": "^0.28.5", 167 | "nanostores": "^1.0.1", 168 | "tsdown": "catalog:" 169 | }, 170 | "dependencies": { 171 | "zod": "^4.1.5" 172 | }, 173 | "peerDependencies": { 174 | "@better-auth/utils": "0.3.0", 175 | "@better-fetch/fetch": "catalog:", 176 | "better-call": "catalog:", 177 | "better-sqlite3": "^12.4.1", 178 | "jose": "^6.1.0", 179 | "kysely": "^0.28.5", 180 | "nanostores": "^1.0.1" 181 | } 182 | } 183 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/jwt/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { JWTPayload } from "jose"; 2 | import type { InferOptionSchema, Session, User } from "../../types"; 3 | import type { Awaitable } from "../../types/helper"; 4 | import type { schema } from "./schema"; 5 | 6 | export interface JwtOptions { 7 | jwks?: { 8 | /** 9 | * Disables the /jwks endpoint and uses this endpoint in discovery. 10 | * 11 | * Useful if jwks are not managed at /jwks or 12 | * if your jwks are signed with a certificate and placed on your CDN. 13 | */ 14 | remoteUrl?: string; 15 | 16 | /** 17 | * Key pair configuration 18 | * @description A subset of the options available for the generateKeyPair function 19 | * 20 | * @see https://github.com/panva/jose/blob/main/src/runtime/node/generate.ts 21 | * 22 | * @default { alg: 'EdDSA', crv: 'Ed25519' } 23 | */ 24 | keyPairConfig?: JWKOptions; 25 | 26 | /** 27 | * Disable private key encryption 28 | * @description Disable the encryption of the private key in the database 29 | * 30 | * @default false 31 | */ 32 | disablePrivateKeyEncryption?: boolean; 33 | }; 34 | 35 | jwt?: { 36 | /** 37 | * The issuer of the JWT 38 | */ 39 | issuer?: string; 40 | /** 41 | * The audience of the JWT 42 | */ 43 | audience?: string; 44 | /** 45 | * Set the "exp" (Expiration Time) Claim. 46 | * 47 | * - If a `number` is passed as an argument it is used as the claim directly. 48 | * - If a `Date` instance is passed as an argument it is converted to unix timestamp and used as the 49 | * claim. 50 | * - If a `string` is passed as an argument it is resolved to a time span, and then added to the 51 | * current unix timestamp and used as the claim. 52 | * 53 | * Format used for time span should be a number followed by a unit, such as "5 minutes" or "1 54 | * day". 55 | * 56 | * Valid units are: "sec", "secs", "second", "seconds", "s", "minute", "minutes", "min", "mins", 57 | * "m", "hour", "hours", "hr", "hrs", "h", "day", "days", "d", "week", "weeks", "w", "year", 58 | * "years", "yr", "yrs", and "y". It is not possible to specify months. 365.25 days is used as an 59 | * alias for a year. 60 | * 61 | * If the string is suffixed with "ago", or prefixed with a "-", the resulting time span gets 62 | * subtracted from the current unix timestamp. A "from now" suffix can also be used for 63 | * readability when adding to the current unix timestamp. 64 | * 65 | * @default 15m 66 | */ 67 | expirationTime?: number | string | Date; 68 | /** 69 | * A function that is called to define the payload of the JWT 70 | */ 71 | definePayload?: (session: { 72 | user: User & Record<string, any>; 73 | session: Session & Record<string, any>; 74 | }) => Promise<Record<string, any>> | Record<string, any>; 75 | /** 76 | * A function that is called to get the subject of the JWT 77 | * 78 | * @default session.user.id 79 | */ 80 | getSubject?: (session: { 81 | user: User & Record<string, any>; 82 | session: Session & Record<string, any>; 83 | }) => Promise<string> | string; 84 | /** 85 | * A custom function to remote sign the jwt payload. 86 | * 87 | * All headers, such as `alg` and `kid`, 88 | * MUST be defined within this function. 89 | * You can safely define the header `typ: 'JWT'`. 90 | * 91 | * @requires jwks.remoteUrl 92 | * @invalidates other jwt.* options 93 | */ 94 | sign?: (payload: JWTPayload) => Awaitable<string>; 95 | }; 96 | 97 | /** 98 | * Disables setting JWTs through middleware. 99 | * 100 | * Recommended to set `true` when using an oAuth provider plugin 101 | * like OIDC or MCP where session payloads should not be signed. 102 | * 103 | * @default false 104 | */ 105 | disableSettingJwtHeader?: boolean; 106 | 107 | /** 108 | * Custom schema for the admin plugin 109 | */ 110 | schema?: InferOptionSchema<typeof schema>; 111 | } 112 | 113 | /** 114 | * Asymmetric (JWS) Supported. 115 | * 116 | * @see https://github.com/panva/jose/issues/210 117 | */ 118 | // JWE is symmetric (ie sharing a secret) thus a jwks is not applicable since there is no public key to share. 119 | // All new JWK "alg" and/or "crv" MUST have an associated test in jwt.test.ts 120 | export type JWKOptions = 121 | | { 122 | alg: "EdDSA"; // EdDSA with Ed25519 key 123 | crv?: "Ed25519"; 124 | } 125 | | { 126 | alg: "ES256"; // ECDSA with P-256 curve 127 | crv?: never; // Only one valid option, no need for crv 128 | } 129 | | { 130 | alg: "ES512"; // ECDSA with P-521 curve 131 | crv?: never; // Only P-521 for ES512 132 | } 133 | | { 134 | alg: "PS256"; // RSA-PSS with SHA-256 135 | modulusLength?: number; // Default to 2048 or higher 136 | } 137 | | { 138 | alg: "RS256"; // RSA with SHA-256 139 | modulusLength?: number; // Default to 2048 or higher 140 | }; 141 | 142 | export type JWSAlgorithms = JWKOptions["alg"]; 143 | 144 | export interface Jwk { 145 | id: string; 146 | publicKey: string; 147 | privateKey: string; 148 | createdAt: Date; 149 | alg?: JWSAlgorithms; 150 | crv?: "Ed25519" | "P-256" | "P-521"; 151 | } 152 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/one-time-token/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { defaultKeyHasher } from ".."; 3 | import { createAuthEndpoint } from "@better-auth/core/api"; 4 | import { sessionMiddleware } from "../../api"; 5 | import { generateRandomString } from "../../crypto"; 6 | import type { BetterAuthPlugin } from "@better-auth/core"; 7 | import type { Session, User } from "../../types"; 8 | import type { GenericEndpointContext } from "@better-auth/core"; 9 | 10 | interface OneTimeTokenOptions { 11 | /** 12 | * Expires in minutes 13 | * 14 | * @default 3 15 | */ 16 | expiresIn?: number; 17 | /** 18 | * Only allow server initiated requests 19 | */ 20 | disableClientRequest?: boolean; 21 | /** 22 | * Generate a custom token 23 | */ 24 | generateToken?: ( 25 | session: { 26 | user: User & Record<string, any>; 27 | session: Session & Record<string, any>; 28 | }, 29 | ctx: GenericEndpointContext, 30 | ) => Promise<string>; 31 | /** 32 | * This option allows you to configure how the token is stored in your database. 33 | * Note: This will not affect the token that's sent, it will only affect the token stored in your database. 34 | * 35 | * @default "plain" 36 | */ 37 | storeToken?: 38 | | "plain" 39 | | "hashed" 40 | | { type: "custom-hasher"; hash: (token: string) => Promise<string> }; 41 | } 42 | 43 | export const oneTimeToken = (options?: OneTimeTokenOptions) => { 44 | const opts = { 45 | storeToken: "plain", 46 | ...options, 47 | } satisfies OneTimeTokenOptions; 48 | 49 | async function storeToken(ctx: GenericEndpointContext, token: string) { 50 | if (opts.storeToken === "hashed") { 51 | return await defaultKeyHasher(token); 52 | } 53 | if ( 54 | typeof opts.storeToken === "object" && 55 | "type" in opts.storeToken && 56 | opts.storeToken.type === "custom-hasher" 57 | ) { 58 | return await opts.storeToken.hash(token); 59 | } 60 | 61 | return token; 62 | } 63 | 64 | return { 65 | id: "one-time-token", 66 | endpoints: { 67 | /** 68 | * ### Endpoint 69 | * 70 | * GET `/one-time-token/generate` 71 | * 72 | * ### API Methods 73 | * 74 | * **server:** 75 | * `auth.api.generateOneTimeToken` 76 | * 77 | * **client:** 78 | * `authClient.oneTimeToken.generate` 79 | * 80 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/one-time-token#api-method-one-time-token-generate) 81 | */ 82 | generateOneTimeToken: createAuthEndpoint( 83 | "/one-time-token/generate", 84 | { 85 | method: "GET", 86 | use: [sessionMiddleware], 87 | }, 88 | async (c) => { 89 | //if request exist, it means it's a client request 90 | if (opts?.disableClientRequest && c.request) { 91 | throw c.error("BAD_REQUEST", { 92 | message: "Client requests are disabled", 93 | }); 94 | } 95 | const session = c.context.session; 96 | const token = opts?.generateToken 97 | ? await opts.generateToken(session, c) 98 | : generateRandomString(32); 99 | const expiresAt = new Date( 100 | Date.now() + (opts?.expiresIn ?? 3) * 60 * 1000, 101 | ); 102 | const storedToken = await storeToken(c, token); 103 | await c.context.internalAdapter.createVerificationValue({ 104 | value: session.session.token, 105 | identifier: `one-time-token:${storedToken}`, 106 | expiresAt, 107 | }); 108 | return c.json({ token }); 109 | }, 110 | ), 111 | /** 112 | * ### Endpoint 113 | * 114 | * POST `/one-time-token/verify` 115 | * 116 | * ### API Methods 117 | * 118 | * **server:** 119 | * `auth.api.verifyOneTimeToken` 120 | * 121 | * **client:** 122 | * `authClient.oneTimeToken.verify` 123 | * 124 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/one-time-token#api-method-one-time-token-verify) 125 | */ 126 | verifyOneTimeToken: createAuthEndpoint( 127 | "/one-time-token/verify", 128 | { 129 | method: "POST", 130 | body: z.object({ 131 | token: z.string().meta({ 132 | description: 'The token to verify. Eg: "some-token"', 133 | }), 134 | }), 135 | }, 136 | async (c) => { 137 | const { token } = c.body; 138 | const storedToken = await storeToken(c, token); 139 | const verificationValue = 140 | await c.context.internalAdapter.findVerificationValue( 141 | `one-time-token:${storedToken}`, 142 | ); 143 | if (!verificationValue) { 144 | throw c.error("BAD_REQUEST", { 145 | message: "Invalid token", 146 | }); 147 | } 148 | await c.context.internalAdapter.deleteVerificationValue( 149 | verificationValue.id, 150 | ); 151 | if (verificationValue.expiresAt < new Date()) { 152 | throw c.error("BAD_REQUEST", { 153 | message: "Token expired", 154 | }); 155 | } 156 | const session = await c.context.internalAdapter.findSession( 157 | verificationValue.value, 158 | ); 159 | if (!session) { 160 | throw c.error("BAD_REQUEST", { 161 | message: "Session not found", 162 | }); 163 | } 164 | return c.json(session); 165 | }, 166 | ), 167 | }, 168 | } satisfies BetterAuthPlugin; 169 | }; 170 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/astro.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Astro Integration 3 | description: Integrate Better Auth with Astro. 4 | --- 5 | 6 | Better Auth comes with first class support for Astro. This guide will show you how to integrate Better Auth with Astro. 7 | 8 | Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation). 9 | 10 | ### Mount the handler 11 | 12 | To enable Better Auth to handle requests, we need to mount the handler to a catch all API route. Create a file inside `/pages/api/auth` called `[...all].ts` and add the following code: 13 | 14 | ```ts title="pages/api/auth/[...all].ts" 15 | import { auth } from "~/auth"; 16 | import type { APIRoute } from "astro"; 17 | 18 | export const ALL: APIRoute = async (ctx) => { 19 | // If you want to use rate limiting, make sure to set the 'x-forwarded-for' header to the request headers from the context 20 | // ctx.request.headers.set("x-forwarded-for", ctx.clientAddress); 21 | return auth.handler(ctx.request); 22 | }; 23 | ``` 24 | 25 | <Callout> 26 | You can change the path on your better-auth configuration but it's recommended to keep it as `/api/auth/[...all]` 27 | </Callout> 28 | 29 | ## Create a client 30 | 31 | Astro supports multiple frontend frameworks, so you can easily import your client based on the framework you're using. 32 | 33 | If you're not using a frontend framework, you can still import the vanilla client. 34 | 35 | 36 | <Tabs items={[ "vanilla", "react", "vue", "svelte", "solid", 37 | ]} defaultValue="react"> 38 | <Tab value="vanilla"> 39 | ```ts title="lib/auth-client.ts" 40 | import { createAuthClient } from "better-auth/client" 41 | export const authClient = createAuthClient() 42 | ``` 43 | </Tab> 44 | <Tab value="react" title="lib/auth-client.ts"> 45 | ```ts title="lib/auth-client.ts" 46 | import { createAuthClient } from "better-auth/react" 47 | export const authClient = createAuthClient() 48 | ``` 49 | </Tab> 50 | <Tab value="vue" title="lib/auth-client.ts"> 51 | ```ts title="lib/auth-client.ts" 52 | import { createAuthClient } from "better-auth/vue" 53 | export const authClient = createAuthClient() 54 | ``` 55 | </Tab> 56 | <Tab value="svelte" title="lib/auth-client.ts"> 57 | ```ts title="lib/auth-client.ts" 58 | import { createAuthClient } from "better-auth/svelte" 59 | export const authClient = createAuthClient() 60 | ``` 61 | </Tab> 62 | <Tab value="solid" title="lib/auth-client.ts"> 63 | ```ts title="lib/auth-client.ts" 64 | import { createAuthClient } from "better-auth/solid" 65 | export const authClient = createAuthClient() 66 | ``` 67 | </Tab> 68 | </Tabs> 69 | 70 | ## Auth Middleware 71 | 72 | ### Astro Locals types 73 | 74 | To have types for your Astro locals, you need to set it inside the `env.d.ts` file. 75 | 76 | ```ts title="env.d.ts" 77 | 78 | /// <reference path="../.astro/types.d.ts" /> 79 | 80 | declare namespace App { 81 | // Note: 'import {} from ""' syntax does not work in .d.ts files. 82 | interface Locals { 83 | user: import("better-auth").User | null; 84 | session: import("better-auth").Session | null; 85 | } 86 | } 87 | ``` 88 | 89 | ### Middleware 90 | 91 | To protect your routes, you can check if the user is authenticated using the `getSession` method in middleware and set the user and session data using the Astro locals with the types we set before. Start by creating a `middleware.ts` file in the root of your project and follow the example below: 92 | 93 | ```ts title="middleware.ts" 94 | import { auth } from "@/auth"; 95 | import { defineMiddleware } from "astro:middleware"; 96 | 97 | export const onRequest = defineMiddleware(async (context, next) => { 98 | const isAuthed = await auth.api 99 | .getSession({ 100 | headers: context.request.headers, 101 | }) 102 | 103 | if (isAuthed) { 104 | context.locals.user = isAuthed.user; 105 | context.locals.session = isAuthed.session; 106 | } else { 107 | context.locals.user = null; 108 | context.locals.session = null; 109 | } 110 | 111 | return next(); 112 | }); 113 | ``` 114 | 115 | ### Getting session on the server inside `.astro` file 116 | 117 | You can use `Astro.locals` to check if the user has session and get the user data from the server side. Here is an example of how you can get the session inside an `.astro` file: 118 | 119 | ```astro 120 | --- 121 | import { UserCard } from "@/components/user-card"; 122 | 123 | const session = () => { 124 | if (Astro.locals.session) { 125 | return Astro.locals.session; 126 | } else { 127 | // Redirect to login page if the user is not authenticated 128 | return Astro.redirect("/login"); 129 | } 130 | } 131 | 132 | --- 133 | 134 | <UserCard initialSession={session} /> 135 | ``` 136 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/line.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import { decodeJwt } from "jose"; 3 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 4 | import { 5 | createAuthorizationURL, 6 | refreshAccessToken, 7 | validateAuthorizationCode, 8 | } from "../oauth2"; 9 | 10 | export interface LineIdTokenPayload { 11 | iss: string; 12 | sub: string; 13 | aud: string; 14 | exp: number; 15 | iat: number; 16 | name?: string; 17 | picture?: string; 18 | email?: string; 19 | amr?: string[]; 20 | nonce?: string; 21 | } 22 | 23 | export interface LineUserInfo { 24 | sub: string; 25 | name?: string; 26 | picture?: string; 27 | email?: string; 28 | } 29 | 30 | export interface LineOptions 31 | extends ProviderOptions<LineUserInfo | LineIdTokenPayload> { 32 | clientId: string; 33 | } 34 | 35 | /** 36 | * LINE Login v2.1 37 | * - Authorization endpoint: https://access.line.me/oauth2/v2.1/authorize 38 | * - Token endpoint: https://api.line.me/oauth2/v2.1/token 39 | * - UserInfo endpoint: https://api.line.me/oauth2/v2.1/userinfo 40 | * - Verify ID token: https://api.line.me/oauth2/v2.1/verify 41 | * 42 | * Docs: https://developers.line.biz/en/reference/line-login/#issue-access-token 43 | */ 44 | export const line = (options: LineOptions) => { 45 | const authorizationEndpoint = "https://access.line.me/oauth2/v2.1/authorize"; 46 | const tokenEndpoint = "https://api.line.me/oauth2/v2.1/token"; 47 | const userInfoEndpoint = "https://api.line.me/oauth2/v2.1/userinfo"; 48 | const verifyIdTokenEndpoint = "https://api.line.me/oauth2/v2.1/verify"; 49 | 50 | return { 51 | id: "line", 52 | name: "LINE", 53 | async createAuthorizationURL({ 54 | state, 55 | scopes, 56 | codeVerifier, 57 | redirectURI, 58 | loginHint, 59 | }) { 60 | const _scopes = options.disableDefaultScope 61 | ? [] 62 | : ["openid", "profile", "email"]; 63 | options.scope && _scopes.push(...options.scope); 64 | scopes && _scopes.push(...scopes); 65 | return await createAuthorizationURL({ 66 | id: "line", 67 | options, 68 | authorizationEndpoint, 69 | scopes: _scopes, 70 | state, 71 | codeVerifier, 72 | redirectURI, 73 | loginHint, 74 | }); 75 | }, 76 | validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { 77 | return validateAuthorizationCode({ 78 | code, 79 | codeVerifier, 80 | redirectURI, 81 | options, 82 | tokenEndpoint, 83 | }); 84 | }, 85 | refreshAccessToken: options.refreshAccessToken 86 | ? options.refreshAccessToken 87 | : async (refreshToken) => { 88 | return refreshAccessToken({ 89 | refreshToken, 90 | options: { 91 | clientId: options.clientId, 92 | clientSecret: options.clientSecret, 93 | }, 94 | tokenEndpoint, 95 | }); 96 | }, 97 | async verifyIdToken(token, nonce) { 98 | if (options.disableIdTokenSignIn) { 99 | return false; 100 | } 101 | if (options.verifyIdToken) { 102 | return options.verifyIdToken(token, nonce); 103 | } 104 | const body = new URLSearchParams(); 105 | body.set("id_token", token); 106 | body.set("client_id", options.clientId); 107 | if (nonce) body.set("nonce", nonce); 108 | const { data, error } = await betterFetch<LineIdTokenPayload>( 109 | verifyIdTokenEndpoint, 110 | { 111 | method: "POST", 112 | headers: { 113 | "content-type": "application/x-www-form-urlencoded", 114 | }, 115 | body, 116 | }, 117 | ); 118 | if (error || !data) { 119 | return false; 120 | } 121 | // aud must match clientId; nonce (if provided) must also match 122 | if (data.aud !== options.clientId) return false; 123 | if (nonce && data.nonce && data.nonce !== nonce) return false; 124 | return true; 125 | }, 126 | async getUserInfo(token) { 127 | if (options.getUserInfo) { 128 | return options.getUserInfo(token); 129 | } 130 | let profile: LineUserInfo | LineIdTokenPayload | null = null; 131 | // Prefer ID token if available 132 | if (token.idToken) { 133 | try { 134 | profile = decodeJwt(token.idToken) as LineIdTokenPayload; 135 | } catch {} 136 | } 137 | // Fallback to UserInfo endpoint 138 | if (!profile) { 139 | const { data } = await betterFetch<LineUserInfo>(userInfoEndpoint, { 140 | headers: { 141 | authorization: `Bearer ${token.accessToken}`, 142 | }, 143 | }); 144 | profile = data || null; 145 | } 146 | if (!profile) return null; 147 | const userMap = await options.mapProfileToUser?.(profile as any); 148 | // ID preference order 149 | const id = (profile as any).sub || (profile as any).userId; 150 | const name = (profile as any).name || (profile as any).displayName; 151 | const image = 152 | (profile as any).picture || (profile as any).pictureUrl || undefined; 153 | const email = (profile as any).email; 154 | return { 155 | user: { 156 | id, 157 | name, 158 | email, 159 | image, 160 | // LINE does not expose email verification status in ID token/userinfo 161 | emailVerified: false, 162 | ...userMap, 163 | }, 164 | data: profile as any, 165 | }; 166 | }, 167 | options, 168 | } satisfies OAuthProvider<LineUserInfo | LineIdTokenPayload, LineOptions>; 169 | }; 170 | ``` -------------------------------------------------------------------------------- /docs/content/docs/concepts/typescript.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: TypeScript 3 | description: Better Auth TypeScript integration. 4 | --- 5 | 6 | Better Auth is designed to be type-safe. Both the client and server are built with TypeScript, allowing you to easily infer types. 7 | 8 | 9 | ## TypeScript Config 10 | 11 | ### Strict Mode 12 | 13 | Better Auth is designed to work with TypeScript's strict mode. We recommend enabling strict mode in your TypeScript config file: 14 | 15 | ```json title="tsconfig.json" 16 | { 17 | "compilerOptions": { 18 | "strict": true 19 | } 20 | } 21 | ``` 22 | 23 | if you can't set `strict` to `true`, you can enable `strictNullChecks`: 24 | 25 | ```json title="tsconfig.json" 26 | { 27 | "compilerOptions": { 28 | "strictNullChecks": true, 29 | } 30 | } 31 | ``` 32 | 33 | <Callout type="warn"> 34 | If you're running into issues with TypeScript inference exceeding maximum length the compiler will serialize, 35 | then please make sure you're following the instructions above, as well as ensuring that both `declaration` and `composite` are not enabled. 36 | </Callout> 37 | 38 | ## Inferring Types 39 | 40 | Both the client SDK and the server offer types that can be inferred using the `$Infer` property. Plugins can extend base types like `User` and `Session`, and you can use `$Infer` to infer these types. Additionally, plugins can provide extra types that can also be inferred through `$Infer`. 41 | 42 | ```ts title="auth-client.ts" 43 | import { createAuthClient } from "better-auth/client" 44 | 45 | const authClient = createAuthClient() 46 | 47 | export type Session = typeof authClient.$Infer.Session 48 | ``` 49 | 50 | The `Session` type includes both `session` and `user` properties. The user property represents the user object type, and the `session` property represents the `session` object type. 51 | 52 | You can also infer types on the server side. 53 | 54 | ```ts title="auth.ts" 55 | import { betterAuth } from "better-auth" 56 | import Database from "better-sqlite3" 57 | 58 | export const auth = betterAuth({ 59 | database: new Database("database.db") 60 | }) 61 | 62 | type Session = typeof auth.$Infer.Session 63 | ``` 64 | 65 | 66 | ## Additional Fields 67 | 68 | Better Auth allows you to add additional fields to the user and session objects. All additional fields are properly inferred and available on the server and client side. 69 | 70 | ```ts 71 | import { betterAuth } from "better-auth" 72 | import Database from "better-sqlite3" 73 | 74 | export const auth = betterAuth({ 75 | database: new Database("database.db"), 76 | user: { 77 | additionalFields: { 78 | role: { 79 | type: "string", 80 | input: false 81 | } 82 | } 83 | } 84 | 85 | }) 86 | 87 | type Session = typeof auth.$Infer.Session 88 | ``` 89 | 90 | In the example above, we added a `role` field to the user object. This field is now available on the `Session` type. 91 | 92 | 93 | ### The `input` property 94 | 95 | The `input` property in an additional field configuration determines whether the field should be included in the user input. This property defaults to `true`, meaning the field will be part of the user input during operations like registration. 96 | 97 | To prevent a field from being part of the user input, you must explicitly set `input: false`: 98 | 99 | ```ts 100 | additionalFields: { 101 | role: { 102 | type: "string", 103 | input: false 104 | } 105 | } 106 | ``` 107 | 108 | When `input` is set to `false`, the field will be excluded from user input, preventing users from passing a value for it. 109 | 110 | By default, additional fields are included in the user input, which can lead to security vulnerabilities if not handled carefully. For fields that should not be set by the user, like a `role`, it is crucial to set `input: false` in the configuration. 111 | 112 | ### Inferring Additional Fields on Client 113 | 114 | To make sure proper type inference for additional fields on the client side, you need to inform the client about these fields. There are two approaches to achieve this, depending on your project structure: 115 | 116 | 1. For Monorepo or Single-Project Setups 117 | 118 | If your server and client code reside in the same project, you can use the `inferAdditionalFields` plugin to automatically infer the additional fields from your server configuration. 119 | 120 | ```ts 121 | import { inferAdditionalFields } from "better-auth/client/plugins"; 122 | import { createAuthClient } from "better-auth/react"; 123 | import type { auth } from "./auth"; 124 | 125 | export const authClient = createAuthClient({ 126 | plugins: [inferAdditionalFields<typeof auth>()], 127 | }); 128 | ``` 129 | 130 | 2. For Separate Client-Server Projects 131 | 132 | If your client and server are in separate projects, you'll need to manually specify the additional fields when creating the auth client. 133 | 134 | ```ts 135 | import { inferAdditionalFields } from "better-auth/client/plugins"; 136 | 137 | export const authClient = createAuthClient({ 138 | plugins: [inferAdditionalFields({ 139 | user: { 140 | role: { 141 | type: "string" 142 | } 143 | } 144 | })], 145 | }); 146 | ``` 147 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/last-login-method/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createAuthMiddleware } from "@better-auth/core/api"; 2 | import type { BetterAuthPlugin } from "@better-auth/core"; 3 | import type { GenericEndpointContext } from "@better-auth/core"; 4 | 5 | /** 6 | * Configuration for tracking different authentication methods 7 | */ 8 | export interface LastLoginMethodOptions { 9 | /** 10 | * Name of the cookie to store the last login method 11 | * @default "better-auth.last_used_login_method" 12 | */ 13 | cookieName?: string; 14 | /** 15 | * Cookie expiration time in seconds 16 | * @default 2592000 (30 days) 17 | */ 18 | maxAge?: number; 19 | /** 20 | * Custom method to resolve the last login method 21 | * @param ctx - The context from the hook 22 | * @returns The last login method 23 | */ 24 | customResolveMethod?: (ctx: GenericEndpointContext) => string | null; 25 | /** 26 | * Store the last login method in the database. This will create a new field in the user table. 27 | * @default false 28 | */ 29 | storeInDatabase?: boolean; 30 | /** 31 | * Custom schema for the plugin 32 | * @default undefined 33 | */ 34 | schema?: { 35 | user?: { 36 | lastLoginMethod?: string; 37 | }; 38 | }; 39 | } 40 | 41 | /** 42 | * Plugin to track the last used login method 43 | */ 44 | export const lastLoginMethod = <O extends LastLoginMethodOptions>( 45 | userConfig?: O, 46 | ) => { 47 | const paths = [ 48 | "/callback/:id", 49 | "/oauth2/callback/:id", 50 | "/sign-in/email", 51 | "/sign-up/email", 52 | ]; 53 | 54 | const defaultResolveMethod = (ctx: GenericEndpointContext) => { 55 | if (paths.includes(ctx.path)) { 56 | return ctx.params?.id ? ctx.params.id : ctx.path.split("/").pop(); 57 | } 58 | return null; 59 | }; 60 | 61 | const config = { 62 | cookieName: "better-auth.last_used_login_method", 63 | maxAge: 60 * 60 * 24 * 30, 64 | ...userConfig, 65 | } satisfies LastLoginMethodOptions; 66 | 67 | return { 68 | id: "last-login-method", 69 | init(ctx) { 70 | return { 71 | options: { 72 | databaseHooks: { 73 | user: { 74 | create: { 75 | async before(user, context) { 76 | if (!config.storeInDatabase) return; 77 | if (!context) return; 78 | const lastUsedLoginMethod = 79 | config.customResolveMethod?.(context) ?? 80 | defaultResolveMethod(context); 81 | if (lastUsedLoginMethod) { 82 | return { 83 | data: { 84 | ...user, 85 | lastLoginMethod: lastUsedLoginMethod, 86 | }, 87 | }; 88 | } 89 | }, 90 | }, 91 | }, 92 | session: { 93 | create: { 94 | async after(session, context) { 95 | if (!config.storeInDatabase) return; 96 | if (!context) return; 97 | const lastUsedLoginMethod = 98 | config.customResolveMethod?.(context) ?? 99 | defaultResolveMethod(context); 100 | if (lastUsedLoginMethod && session?.userId) { 101 | try { 102 | await ctx.internalAdapter.updateUser(session.userId, { 103 | lastLoginMethod: lastUsedLoginMethod, 104 | }); 105 | } catch (error) { 106 | ctx.logger.error( 107 | "Failed to update lastLoginMethod", 108 | error, 109 | ); 110 | } 111 | } 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | }; 118 | }, 119 | hooks: { 120 | after: [ 121 | { 122 | matcher() { 123 | return true; 124 | }, 125 | handler: createAuthMiddleware(async (ctx) => { 126 | const lastUsedLoginMethod = 127 | config.customResolveMethod?.(ctx) ?? defaultResolveMethod(ctx); 128 | if (lastUsedLoginMethod) { 129 | const setCookie = ctx.context.responseHeaders?.get("set-cookie"); 130 | const sessionTokenName = 131 | ctx.context.authCookies.sessionToken.name; 132 | const hasSessionToken = 133 | setCookie && setCookie.includes(sessionTokenName); 134 | if (hasSessionToken) { 135 | // Inherit cookie attributes from Better Auth's centralized cookie system 136 | // This ensures consistency with cross-origin, cross-subdomain, and security settings 137 | const cookieAttributes = { 138 | ...ctx.context.authCookies.sessionToken.options, 139 | maxAge: config.maxAge, 140 | httpOnly: false, // Override: plugin cookies are not httpOnly 141 | }; 142 | 143 | ctx.setCookie( 144 | config.cookieName, 145 | lastUsedLoginMethod, 146 | cookieAttributes, 147 | ); 148 | } 149 | } 150 | }), 151 | }, 152 | ], 153 | }, 154 | schema: (config.storeInDatabase 155 | ? { 156 | user: { 157 | fields: { 158 | lastLoginMethod: { 159 | type: "string", 160 | input: false, 161 | required: false, 162 | fieldName: 163 | config.schema?.user?.lastLoginMethod || "lastLoginMethod", 164 | }, 165 | }, 166 | }, 167 | } 168 | : undefined) as O["storeInDatabase"] extends true 169 | ? { 170 | user: { 171 | fields: { 172 | lastLoginMethod: { 173 | type: "string"; 174 | required: false; 175 | input: false; 176 | }; 177 | }; 178 | }; 179 | } 180 | : undefined, 181 | } satisfies BetterAuthPlugin; 182 | }; 183 | ``` -------------------------------------------------------------------------------- /docs/content/changelogs/1-2.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: 1.2 Release 3 | description: Stripe, Captcha, API Keys, Teams, Init CLI, and more. 4 | date: 2025-03-01 5 | --- 6 | 7 | # Better Auth 1.2 – Stripe, Captcha, API Keys, Teams, Init CLI, and more 8 | 9 | To upgrade, run: 10 | 11 | ```package-install 12 | npm install [email protected] 13 | ``` 14 | 15 | --- 16 | 17 | ### **Stripe Plugin (Beta)** 18 | 19 | Stripe integration for customer management, subscriptions, and webhooks. 20 | 21 | ```package-install 22 | npm install @better-auth/stripe 23 | ``` 24 | 25 | ```ts title="auth.ts" 26 | import { stripe } from "@better-auth/stripe"; // [!code highlight] 27 | 28 | export const auth = betterAuth({ 29 | plugins: [ 30 | stripe({ 31 | // [!code highlight] 32 | createCustomerOnSignup: true, // [!code highlight] 33 | subscription: { // [!code highlight] 34 | enabled: true, // [!code highlight] 35 | plans: [// [!code highlight] 36 | { // [!code highlight] 37 | name: "pro", // [!code highlight] 38 | priceId: "price_1234567890", // [!code highlight] 39 | }, // [!code highlight] 40 | ], // [!code highlight] 41 | }, // [!code highlight] 42 | }), // [!code highlight] 43 | ], 44 | }); 45 | ``` 46 | 47 | Read the [Stripe Plugin docs](/docs/plugins/stripe) for more information. 48 | 49 | ### **Captcha Plugin** 50 | 51 | Protect your authentication flows with Google reCAPTCHA and Cloudflare Turnstile. Works for signup, signin, and password resets. 52 | 53 | ```ts title="auth.ts" 54 | import { captcha } from "better-auth/plugins"; 55 | 56 | const auth = betterAuth({ 57 | plugins: [ 58 | // [!code highlight] 59 | captcha({ 60 | // [!code highlight] 61 | provider: "cloudflare-turnstile", // or "google-recaptcha" // [!code highlight] 62 | secretKey: process.env.TURNSTILE_SECRET_KEY!, // [!code highlight] 63 | }), // [!code highlight] 64 | ], // [!code highlight] 65 | }); 66 | ``` 67 | 68 | Read the [Captcha Plugin docs](/docs/plugins/captcha) for more information. 69 | 70 | ### **API Key Plugin** 71 | 72 | Generate and manage API keys with rate limiting, expiration, and metadata. Supports session creation from API keys. 73 | 74 | ```ts title="auth.ts" 75 | import { apiKey } from "better-auth/plugins"; 76 | 77 | const auth = betterAuth({ 78 | plugins: [apiKey()], 79 | }); 80 | ``` 81 | 82 | Read the [API Key Plugin docs](/docs/plugins/api-key) for more information. 83 | 84 | ### **Teams/Sub-Organizations** 85 | 86 | Organizations can now have teams or sub-organizations under them. 87 | 88 | ```ts title="auth.ts" 89 | const auth = betterAuth({ 90 | plugins: [ 91 | organization({ 92 | teams: { 93 | enabled: true, 94 | }, 95 | }), 96 | ], 97 | }); 98 | ``` 99 | 100 | Read the [Organization Plugin docs](/docs/plugins/organization#teams) for more information. 101 | 102 | ### **Init CLI** 103 | 104 | The CLI now includes an `init` command to add Better Auth to your project. 105 | 106 | ```bash title="terminal" 107 | npx @better-auth/cli init 108 | ``` 109 | 110 | ### **Username** 111 | 112 | - Added `displayName` for case-insensitive lookups while preserving original formatting. 113 | - Built-in validation. 114 | 115 | <Callout type="info"> 116 | If you're using the Username plugin, make sure to add the `displayName` field 117 | to your schema. 118 | </Callout> 119 | 120 | ### **Organization** 121 | 122 | - **Multiple Roles per User** – Assign more than one role to a user. 123 | 124 | ### **Admin Plugin** 125 | 126 | - Manage roles and permissions within the admin plugin. [Learn more](/docs/plugins/admin) 127 | - `adminUserIds` option to grant specific users admin privileges. [Learn more](/docs/plugins/admin#usage) 128 | 129 | --- 130 | 131 | ## 🎭 New Social Providers 132 | 133 | - [TikTok](/docs/authentication/tiktok) 134 | - [Roblox](/docs/authentication/roblox) 135 | - [VK](/docs/authentication/vk) 136 | 137 | --- 138 | 139 | ## ✨ Core Enhancements 140 | 141 | - **Auto Cleanup** for expired verification data 142 | - **Improved Google One Tap** integration with JWT verification and enhanced prompt handling 143 | - **Phone-based Password Reset** functionality 144 | - **Provider Control Options**: 145 | - Disable signups for specific providers 146 | - Disable implicit signups for specific providers 147 | - Control default scopes and allow custom scopes on request 148 | - **Enhanced Database Hooks** with additional context information 149 | 150 | --- 151 | 152 | ## 🚀 Performance Boosts 153 | 154 | We rewrote **better-call** (the core library behind Better Auth) to fix TypeScript editor lag. Your IDE should now feel much snappier when working with Better Auth. 155 | 156 | --- 157 | 158 | ## ⚡ CLI Enhancements 159 | 160 | ### **`init` Command** 161 | 162 | The CLI now includes an `init` command to speed up setup: 163 | 164 | - Scaffold new projects 165 | - Generate schemas 166 | - Run migrations 167 | 168 | [Learn more](/docs/concepts/cli) 169 | 170 | --- 171 | 172 | ## 🛠 Bug Fixes & Stability Improvements 173 | 174 | A lot of fixes and refinements to make everything smoother, faster, and more reliable. Check out the [changelog](https://github.com/better-auth/better-auth/releases/tag/v1.2.0) for more details. 175 | 176 | --- 177 | 178 | ```package-install 179 | npm install [email protected] 180 | ``` 181 | 182 | **Upgrade now and take advantage of these powerful new features!** 🚀 183 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/toast.tsx: -------------------------------------------------------------------------------- ```typescript 1 | // @ts-nocheck 2 | "use client"; 3 | 4 | import * as React from "react"; 5 | import { Cross2Icon } from "@radix-ui/react-icons"; 6 | import * as ToastPrimitives from "@radix-ui/react-toast"; 7 | import { cva } from "class-variance-authority"; 8 | 9 | import { cn } from "@/lib/utils"; 10 | 11 | const ToastProvider = ToastPrimitives.Provider; 12 | 13 | const ToastViewport = ({ 14 | ref, 15 | className, 16 | ...props 17 | }: React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> & { 18 | ref: React.RefObject<React.ElementRef<typeof ToastPrimitives.Viewport>>; 19 | }) => ( 20 | <ToastPrimitives.Viewport 21 | ref={ref} 22 | className={cn( 23 | "fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", 24 | className, 25 | )} 26 | {...props} 27 | /> 28 | ); 29 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 30 | 31 | const toastVariants = cva( 32 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 33 | { 34 | variants: { 35 | variant: { 36 | default: "border bg-background text-foreground", 37 | destructive: 38 | "destructive group border-destructive bg-destructive text-destructive-foreground", 39 | }, 40 | }, 41 | defaultVariants: { 42 | variant: "default", 43 | }, 44 | }, 45 | ); 46 | 47 | const Toast = ({ ref, className, variant, ...props }) => { 48 | return ( 49 | <ToastPrimitives.Root 50 | ref={ref} 51 | className={cn(toastVariants({ variant }), className)} 52 | {...props} 53 | /> 54 | ); 55 | }; 56 | Toast.displayName = ToastPrimitives.Root.displayName; 57 | 58 | const ToastAction = ({ 59 | ref, 60 | className, 61 | ...props 62 | }: React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> & { 63 | ref: React.RefObject<React.ElementRef<typeof ToastPrimitives.Action>>; 64 | }) => ( 65 | <ToastPrimitives.Action 66 | ref={ref} 67 | className={cn( 68 | "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", 69 | className, 70 | )} 71 | {...props} 72 | /> 73 | ); 74 | ToastAction.displayName = ToastPrimitives.Action.displayName; 75 | 76 | const ToastClose = ({ 77 | ref, 78 | className, 79 | ...props 80 | }: React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> & { 81 | ref: React.RefObject<React.ElementRef<typeof ToastPrimitives.Close>>; 82 | }) => ( 83 | <ToastPrimitives.Close 84 | ref={ref} 85 | className={cn( 86 | "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", 87 | className, 88 | )} 89 | toast-close="" 90 | {...props} 91 | > 92 | <Cross2Icon className="h-4 w-4" /> 93 | </ToastPrimitives.Close> 94 | ); 95 | ToastClose.displayName = ToastPrimitives.Close.displayName; 96 | 97 | const ToastTitle = ({ 98 | ref, 99 | className, 100 | ...props 101 | }: React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> & { 102 | ref: React.RefObject<React.ElementRef<typeof ToastPrimitives.Title>>; 103 | }) => ( 104 | <ToastPrimitives.Title 105 | ref={ref} 106 | className={cn("text-sm font-semibold [&+div]:text-xs", className)} 107 | {...props} 108 | /> 109 | ); 110 | ToastTitle.displayName = ToastPrimitives.Title.displayName; 111 | 112 | const ToastDescription = ({ 113 | ref, 114 | className, 115 | ...props 116 | }: React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> & { 117 | ref: React.RefObject<React.ElementRef<typeof ToastPrimitives.Description>>; 118 | }) => ( 119 | <ToastPrimitives.Description 120 | ref={ref} 121 | className={cn("text-sm opacity-90", className)} 122 | {...props} 123 | /> 124 | ); 125 | ToastDescription.displayName = ToastPrimitives.Description.displayName; 126 | 127 | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>; 128 | 129 | type ToastActionElement = React.ReactElement<typeof ToastAction>; 130 | 131 | export { 132 | type ToastProps, 133 | type ToastActionElement, 134 | ToastProvider, 135 | ToastViewport, 136 | Toast, 137 | ToastTitle, 138 | ToastDescription, 139 | ToastClose, 140 | ToastAction, 141 | }; 142 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/facebook.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2"; 4 | import { createRemoteJWKSet, jwtVerify, decodeJwt } from "jose"; 5 | import { refreshAccessToken } from "../oauth2"; 6 | export interface FacebookProfile { 7 | id: string; 8 | name: string; 9 | email: string; 10 | email_verified: boolean; 11 | picture: { 12 | data: { 13 | height: number; 14 | is_silhouette: boolean; 15 | url: string; 16 | width: number; 17 | }; 18 | }; 19 | } 20 | 21 | export interface FacebookOptions extends ProviderOptions<FacebookProfile> { 22 | clientId: string; 23 | /** 24 | * Extend list of fields to retrieve from the Facebook user profile. 25 | * 26 | * @default ["id", "name", "email", "picture"] 27 | */ 28 | fields?: string[]; 29 | 30 | /** 31 | * The config id to use when undergoing oauth 32 | */ 33 | configId?: string; 34 | } 35 | 36 | export const facebook = (options: FacebookOptions) => { 37 | return { 38 | id: "facebook", 39 | name: "Facebook", 40 | async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) { 41 | const _scopes = options.disableDefaultScope 42 | ? [] 43 | : ["email", "public_profile"]; 44 | options.scope && _scopes.push(...options.scope); 45 | scopes && _scopes.push(...scopes); 46 | return await createAuthorizationURL({ 47 | id: "facebook", 48 | options, 49 | authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth", 50 | scopes: _scopes, 51 | state, 52 | redirectURI, 53 | loginHint, 54 | additionalParams: options.configId 55 | ? { 56 | config_id: options.configId, 57 | } 58 | : {}, 59 | }); 60 | }, 61 | validateAuthorizationCode: async ({ code, redirectURI }) => { 62 | return validateAuthorizationCode({ 63 | code, 64 | redirectURI, 65 | options, 66 | tokenEndpoint: "https://graph.facebook.com/oauth/access_token", 67 | }); 68 | }, 69 | async verifyIdToken(token, nonce) { 70 | if (options.disableIdTokenSignIn) { 71 | return false; 72 | } 73 | 74 | if (options.verifyIdToken) { 75 | return options.verifyIdToken(token, nonce); 76 | } 77 | 78 | /* limited login */ 79 | // check is limited token 80 | if (token.split(".").length === 3) { 81 | try { 82 | const { payload: jwtClaims } = await jwtVerify( 83 | token, 84 | createRemoteJWKSet( 85 | // https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks 86 | new URL( 87 | "https://limited.facebook.com/.well-known/oauth/openid/jwks/", 88 | ), 89 | ), 90 | { 91 | algorithms: ["RS256"], 92 | audience: options.clientId, 93 | issuer: "https://www.facebook.com", 94 | }, 95 | ); 96 | 97 | if (nonce && jwtClaims.nonce !== nonce) { 98 | return false; 99 | } 100 | 101 | return !!jwtClaims; 102 | } catch (error) { 103 | return false; 104 | } 105 | } 106 | 107 | /* access_token */ 108 | return true; 109 | }, 110 | refreshAccessToken: options.refreshAccessToken 111 | ? options.refreshAccessToken 112 | : async (refreshToken) => { 113 | return refreshAccessToken({ 114 | refreshToken, 115 | options: { 116 | clientId: options.clientId, 117 | clientKey: options.clientKey, 118 | clientSecret: options.clientSecret, 119 | }, 120 | tokenEndpoint: 121 | "https://graph.facebook.com/v18.0/oauth/access_token", 122 | }); 123 | }, 124 | async getUserInfo(token) { 125 | if (options.getUserInfo) { 126 | return options.getUserInfo(token); 127 | } 128 | 129 | if (token.idToken && token.idToken.split(".").length === 3) { 130 | const profile = decodeJwt(token.idToken) as { 131 | sub: string; 132 | email: string; 133 | name: string; 134 | picture: string; 135 | }; 136 | 137 | const user = { 138 | id: profile.sub, 139 | name: profile.name, 140 | email: profile.email, 141 | picture: { 142 | data: { 143 | url: profile.picture, 144 | height: 100, 145 | width: 100, 146 | is_silhouette: false, 147 | }, 148 | }, 149 | }; 150 | 151 | // https://developers.facebook.com/docs/facebook-login/limited-login/permissions 152 | const userMap = await options.mapProfileToUser?.({ 153 | ...user, 154 | email_verified: true, 155 | }); 156 | 157 | return { 158 | user: { 159 | ...user, 160 | emailVerified: true, 161 | ...userMap, 162 | }, 163 | data: profile, 164 | }; 165 | } 166 | 167 | const fields = [ 168 | "id", 169 | "name", 170 | "email", 171 | "picture", 172 | ...(options?.fields || []), 173 | ]; 174 | const { data: profile, error } = await betterFetch<FacebookProfile>( 175 | "https://graph.facebook.com/me?fields=" + fields.join(","), 176 | { 177 | auth: { 178 | type: "Bearer", 179 | token: token.accessToken, 180 | }, 181 | }, 182 | ); 183 | if (error) { 184 | return null; 185 | } 186 | const userMap = await options.mapProfileToUser?.(profile); 187 | return { 188 | user: { 189 | id: profile.id, 190 | name: profile.name, 191 | email: profile.email, 192 | image: profile.picture.data.url, 193 | emailVerified: profile.email_verified, 194 | ...userMap, 195 | }, 196 | data: profile, 197 | }; 198 | }, 199 | options, 200 | } satisfies OAuthProvider<FacebookProfile>; 201 | }; 202 | ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/changelog-layout.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Link from "next/link"; 2 | 3 | import clsx from "clsx"; 4 | import { DiscordLogoIcon } from "@radix-ui/react-icons"; 5 | 6 | function BookIcon(props: React.ComponentPropsWithoutRef<"svg">) { 7 | return ( 8 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 9 | <path d="M7 3.41a1 1 0 0 0-.668-.943L2.275 1.039a.987.987 0 0 0-.877.166c-.25.192-.398.493-.398.812V12.2c0 .454.296.853.725.977l3.948 1.365A1 1 0 0 0 7 13.596V3.41ZM9 13.596a1 1 0 0 0 1.327.946l3.948-1.365c.429-.124.725-.523.725-.977V2.017c0-.32-.147-.62-.398-.812a.987.987 0 0 0-.877-.166L9.668 2.467A1 1 0 0 0 9 3.41v10.186Z" /> 10 | </svg> 11 | ); 12 | } 13 | 14 | function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { 15 | return ( 16 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 17 | <path d="M8 .198a8 8 0 0 0-8 8 7.999 7.999 0 0 0 5.47 7.59c.4.076.547-.172.547-.384 0-.19-.007-.694-.01-1.36-2.226.482-2.695-1.074-2.695-1.074-.364-.923-.89-1.17-.89-1.17-.725-.496.056-.486.056-.486.803.056 1.225.824 1.225.824.714 1.224 1.873.87 2.33.666.072-.518.278-.87.507-1.07-1.777-.2-3.644-.888-3.644-3.954 0-.873.31-1.586.823-2.146-.09-.202-.36-1.016.07-2.118 0 0 .67-.214 2.2.82a7.67 7.67 0 0 1 2-.27 7.67 7.67 0 0 1 2 .27c1.52-1.034 2.19-.82 2.19-.82.43 1.102.16 1.916.08 2.118.51.56.82 1.273.82 2.146 0 3.074-1.87 3.75-3.65 3.947.28.24.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.14.46.55.38A7.972 7.972 0 0 0 16 8.199a8 8 0 0 0-8-8Z" /> 18 | </svg> 19 | ); 20 | } 21 | 22 | function FeedIcon(props: React.ComponentPropsWithoutRef<"svg">) { 23 | return ( 24 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 25 | <path 26 | fillRule="evenodd" 27 | clipRule="evenodd" 28 | d="M2.5 3a.5.5 0 0 1 .5-.5h.5c5.523 0 10 4.477 10 10v.5a.5.5 0 0 1-.5.5h-.5a.5.5 0 0 1-.5-.5v-.5A8.5 8.5 0 0 0 3.5 4H3a.5.5 0 0 1-.5-.5V3Zm0 4.5A.5.5 0 0 1 3 7h.5A5.5 5.5 0 0 1 9 12.5v.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-.5a4 4 0 0 0-4-4H3a.5.5 0 0 1-.5-.5v-.5Zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z" 29 | /> 30 | </svg> 31 | ); 32 | } 33 | 34 | function XIcon(props: React.ComponentPropsWithoutRef<"svg">) { 35 | return ( 36 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 37 | <path d="M9.51762 6.77491L15.3459 0H13.9648L8.90409 5.88256L4.86212 0H0.200195L6.31244 8.89547L0.200195 16H1.58139L6.92562 9.78782L11.1942 16H15.8562L9.51728 6.77491H9.51762ZM7.62588 8.97384L7.00658 8.08805L2.07905 1.03974H4.20049L8.17706 6.72795L8.79636 7.61374L13.9654 15.0075H11.844L7.62588 8.97418V8.97384Z" /> 38 | </svg> 39 | ); 40 | } 41 | 42 | export function Intro() { 43 | return ( 44 | <> 45 | <h1 className="mt-14 font-sans font-semibold tracking-tighter text-5xl"> 46 | All of the changes made will be{" "} 47 | <span className="">available here.</span> 48 | </h1> 49 | <p className="mt-4 text-sm text-gray-600 dark:text-gray-300"> 50 | Better Auth is comprehensive authentication library for TypeScript that 51 | provides a wide range of features to make authentication easier and more 52 | secure. 53 | </p> 54 | <hr className="h-px bg-gray-300 mt-5" /> 55 | <div className="mt-8 flex flex-wrap text-gray-600 dark:text-gray-300 justify-center gap-x-1 gap-y-3 sm:gap-x-2 lg:justify-start"> 56 | <IconLink 57 | href="/docs" 58 | icon={BookIcon} 59 | className="flex-none text-gray-600 dark:text-gray-300" 60 | > 61 | Documentation 62 | </IconLink> 63 | <IconLink 64 | href="https://github.com/better-auth/better-auth" 65 | icon={GitHubIcon} 66 | className="flex-none text-gray-600 dark:text-gray-300" 67 | > 68 | GitHub 69 | </IconLink> 70 | <IconLink 71 | href="https://discord.gg/better-auth" 72 | icon={DiscordLogoIcon} 73 | className="flex-none text-gray-600 dark:text-gray-300" 74 | > 75 | Community 76 | </IconLink> 77 | </div> 78 | </> 79 | ); 80 | } 81 | 82 | export function IntroFooter() { 83 | return ( 84 | <p className="flex items-baseline gap-x-2 text-[0.8125rem]/6 text-gray-500"> 85 | Brought to you by{" "} 86 | <IconLink href="#" icon={XIcon} compact> 87 | BETTER-AUTH. 88 | </IconLink> 89 | </p> 90 | ); 91 | } 92 | 93 | export function IconLink({ 94 | children, 95 | className, 96 | compact = false, 97 | icon: Icon, 98 | ...props 99 | }: React.ComponentPropsWithoutRef<typeof Link> & { 100 | compact?: boolean; 101 | icon?: React.ComponentType<{ className?: string }>; 102 | }) { 103 | return ( 104 | <Link 105 | {...props} 106 | className={clsx( 107 | className, 108 | "group relative isolate flex items-center px-2 py-0.5 text-[0.8125rem]/6 font-medium text-black/70 dark:text-white/30 transition-colors hover:text-stone-300 rounded-none", 109 | compact ? "gap-x-2" : "gap-x-3", 110 | )} 111 | > 112 | <span className="absolute inset-0 -z-10 scale-75 rounded-lg bg-white/5 opacity-0 transition group-hover:scale-100 group-hover:opacity-100" /> 113 | {Icon && <Icon className="h-4 w-4 flex-none" />} 114 | <span className="self-baseline text-black/70 dark:text-white"> 115 | {children} 116 | </span> 117 | </Link> 118 | ); 119 | } 120 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/custom-session/custom-session.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, expectTypeOf, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { customSession } from "."; 4 | import { admin } from "../admin"; 5 | import { createAuthClient } from "../../client"; 6 | import { customSessionClient } from "./client"; 7 | import type { BetterAuthOptions } from "../../types"; 8 | import { adminClient } from "../admin/client"; 9 | import { multiSession } from "../multi-session"; 10 | import { multiSessionClient } from "../multi-session/client"; 11 | import { parseSetCookieHeader } from "../../cookies"; 12 | 13 | describe("Custom Session Plugin Tests", async () => { 14 | const options = { 15 | plugins: [admin(), multiSession()], 16 | } satisfies BetterAuthOptions; 17 | const { auth, signInWithTestUser, testUser, customFetchImpl, cookieSetter } = 18 | await getTestInstance({ 19 | session: { 20 | maxAge: 10, 21 | updateAge: 0, 22 | cookieCache: { 23 | enabled: true, 24 | maxAge: 10, 25 | }, 26 | }, 27 | plugins: [ 28 | ...options.plugins, 29 | customSession( 30 | async ({ user, session }) => { 31 | const newData = { 32 | message: "Hello, World!", 33 | }; 34 | return { 35 | user: { 36 | firstName: user.name.split(" ")[0], 37 | lastName: user.name.split(" ")[1], 38 | }, 39 | newData, 40 | session, 41 | }; 42 | }, 43 | options, 44 | { shouldMutateListDeviceSessionsEndpoint: true }, 45 | ), 46 | ], 47 | }); 48 | 49 | const client = createAuthClient({ 50 | baseURL: "http://localhost:3000", 51 | plugins: [ 52 | customSessionClient<typeof auth>(), 53 | adminClient(), 54 | multiSessionClient(), 55 | ], 56 | fetchOptions: { customFetchImpl }, 57 | }); 58 | 59 | it("should return the session", async () => { 60 | const { headers } = await signInWithTestUser(); 61 | const session = await auth.api.getSession({ headers }); 62 | const s = await client.getSession({ fetchOptions: { headers } }); 63 | expect(s.data?.newData).toEqual({ message: "Hello, World!" }); 64 | expect(session?.newData).toEqual({ message: "Hello, World!" }); 65 | }); 66 | 67 | it("should return set cookie headers", async () => { 68 | const { headers } = await signInWithTestUser(); 69 | const s = await client.getSession({ 70 | fetchOptions: { 71 | headers, 72 | onResponse(context) { 73 | const header = context.response.headers.get("set-cookie"); 74 | expect(header).toBeDefined(); 75 | 76 | const cookies = parseSetCookieHeader(header!); 77 | expect(cookies.has("better-auth.session_token")).toBe(true); 78 | expect(cookies.has("better-auth.session_data")).toBe(true); 79 | }, 80 | }, 81 | }); 82 | }); 83 | 84 | it("should return the custom session for multi-session", async () => { 85 | let headers = new Headers(); 86 | const testUser = { 87 | email: "[email protected]", 88 | password: "password", 89 | name: "Name", 90 | }; 91 | 92 | await client.signUp.email( 93 | { 94 | name: testUser.name, 95 | email: testUser.email, 96 | password: testUser.password, 97 | }, 98 | { 99 | onSuccess: cookieSetter(headers), 100 | }, 101 | ); 102 | const sessions = await auth.api.listDeviceSessions({ 103 | headers, 104 | }); 105 | const session = sessions[0]!; 106 | //@ts-expect-error 107 | expect(session.newData).toEqual({ message: "Hello, World!" }); 108 | }); 109 | 110 | it.skipIf(globalThis.gc == null)( 111 | "should not create memory leaks with multiple plugin instances", 112 | async () => { 113 | const initialMemory = process.memoryUsage(); 114 | 115 | const pluginInstances = []; 116 | const sessionCount = 100; 117 | 118 | for (let i = 0; i < sessionCount; i++) { 119 | const plugin = customSession(async ({ user, session }) => { 120 | return { 121 | user: { 122 | ...user, 123 | testField: `test-${i}`, 124 | }, 125 | session, 126 | iteration: i, 127 | }; 128 | }); 129 | pluginInstances.push(plugin); 130 | } 131 | // Force garbage collection (only works if Node.js is started with --expose-gc) 132 | // @ts-expect-error 133 | globalThis.gc(); 134 | 135 | const afterPluginCreation = process.memoryUsage(); 136 | 137 | const memoryIncrease = 138 | afterPluginCreation.heapUsed - initialMemory.heapUsed; 139 | const memoryIncreasePerPlugin = memoryIncrease / sessionCount; 140 | // Each plugin instance should not use more than <5KB of memory 141 | // (this is a reasonable threshold that indicates no major memory leak) 142 | expect(memoryIncreasePerPlugin).toBeLessThan(5 * 1024); 143 | // Verify that plugins are still functional 144 | expect(pluginInstances).toHaveLength(sessionCount); 145 | expect(pluginInstances[0]!.id).toBe("custom-session"); 146 | expect(pluginInstances[sessionCount - 1]!.id).toBe("custom-session"); 147 | }, 148 | ); 149 | 150 | it("should infer the session type", async () => { 151 | const { auth } = await getTestInstance({ 152 | plugins: [ 153 | customSession(async ({ user, session }) => { 154 | return { 155 | custom: { 156 | field: "field", 157 | }, 158 | }; 159 | }), 160 | ], 161 | }); 162 | type Session = typeof auth.$Infer.Session; 163 | 164 | expectTypeOf<Session>().toEqualTypeOf<{ 165 | custom: { 166 | field: string; 167 | }; 168 | }>(); 169 | }); 170 | }); 171 | ``` -------------------------------------------------------------------------------- /packages/core/src/oauth2/oauth-provider.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { LiteralString } from "../types"; 2 | 3 | export interface OAuth2Tokens { 4 | tokenType?: string; 5 | accessToken?: string; 6 | refreshToken?: string; 7 | accessTokenExpiresAt?: Date; 8 | refreshTokenExpiresAt?: Date; 9 | scopes?: string[]; 10 | idToken?: string; 11 | } 12 | 13 | export type OAuth2UserInfo = { 14 | id: string | number; 15 | name?: string; 16 | email?: string | null; 17 | image?: string; 18 | emailVerified: boolean; 19 | }; 20 | 21 | export interface OAuthProvider< 22 | T extends Record<string, any> = Record<string, any>, 23 | O extends Record<string, any> = Partial<ProviderOptions>, 24 | > { 25 | id: LiteralString; 26 | createAuthorizationURL: (data: { 27 | state: string; 28 | codeVerifier: string; 29 | scopes?: string[]; 30 | redirectURI: string; 31 | display?: string; 32 | loginHint?: string; 33 | }) => Promise<URL> | URL; 34 | name: string; 35 | validateAuthorizationCode: (data: { 36 | code: string; 37 | redirectURI: string; 38 | codeVerifier?: string; 39 | deviceId?: string; 40 | }) => Promise<OAuth2Tokens>; 41 | getUserInfo: ( 42 | token: OAuth2Tokens & { 43 | /** 44 | * The user object from the provider 45 | * This is only available for some providers like Apple 46 | */ 47 | user?: { 48 | name?: { 49 | firstName?: string; 50 | lastName?: string; 51 | }; 52 | email?: string; 53 | }; 54 | }, 55 | ) => Promise<{ 56 | user: OAuth2UserInfo; 57 | data: T; 58 | } | null>; 59 | /** 60 | * Custom function to refresh a token 61 | */ 62 | refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>; 63 | revokeToken?: (token: string) => Promise<void>; 64 | /** 65 | * Verify the id token 66 | * @param token - The id token 67 | * @param nonce - The nonce 68 | * @returns True if the id token is valid, false otherwise 69 | */ 70 | verifyIdToken?: (token: string, nonce?: string) => Promise<boolean>; 71 | /** 72 | * Disable implicit sign up for new users. When set to true for the provider, 73 | * sign-in need to be called with with requestSignUp as true to create new users. 74 | */ 75 | disableImplicitSignUp?: boolean; 76 | /** 77 | * Disable sign up for new users. 78 | */ 79 | disableSignUp?: boolean; 80 | /** 81 | * Options for the provider 82 | */ 83 | options?: O; 84 | } 85 | 86 | export type ProviderOptions<Profile extends Record<string, any> = any> = { 87 | /** 88 | * The client ID of your application. 89 | * 90 | * This is usually a string but can be any type depending on the provider. 91 | */ 92 | clientId?: unknown; 93 | /** 94 | * The client secret of your application 95 | */ 96 | clientSecret?: string; 97 | /** 98 | * The scopes you want to request from the provider 99 | */ 100 | scope?: string[]; 101 | /** 102 | * Remove default scopes of the provider 103 | */ 104 | disableDefaultScope?: boolean; 105 | /** 106 | * The redirect URL for your application. This is where the provider will 107 | * redirect the user after the sign in process. Make sure this URL is 108 | * whitelisted in the provider's dashboard. 109 | */ 110 | redirectURI?: string; 111 | /** 112 | * The client key of your application 113 | * Tiktok Social Provider uses this field instead of clientId 114 | */ 115 | clientKey?: string; 116 | /** 117 | * Disable provider from allowing users to sign in 118 | * with this provider with an id token sent from the 119 | * client. 120 | */ 121 | disableIdTokenSignIn?: boolean; 122 | /** 123 | * verifyIdToken function to verify the id token 124 | */ 125 | verifyIdToken?: (token: string, nonce?: string) => Promise<boolean>; 126 | /** 127 | * Custom function to get user info from the provider 128 | */ 129 | getUserInfo?: (token: OAuth2Tokens) => Promise<{ 130 | user: { 131 | id: string; 132 | name?: string; 133 | email?: string | null; 134 | image?: string; 135 | emailVerified: boolean; 136 | [key: string]: any; 137 | }; 138 | data: any; 139 | }>; 140 | /** 141 | * Custom function to refresh a token 142 | */ 143 | refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>; 144 | /** 145 | * Custom function to map the provider profile to a 146 | * user. 147 | */ 148 | mapProfileToUser?: (profile: Profile) => 149 | | { 150 | id?: string; 151 | name?: string; 152 | email?: string | null; 153 | image?: string; 154 | emailVerified?: boolean; 155 | [key: string]: any; 156 | } 157 | | Promise<{ 158 | id?: string; 159 | name?: string; 160 | email?: string | null; 161 | image?: string; 162 | emailVerified?: boolean; 163 | [key: string]: any; 164 | }>; 165 | /** 166 | * Disable implicit sign up for new users. When set to true for the provider, 167 | * sign-in need to be called with with requestSignUp as true to create new users. 168 | */ 169 | disableImplicitSignUp?: boolean; 170 | /** 171 | * Disable sign up for new users. 172 | */ 173 | disableSignUp?: boolean; 174 | /** 175 | * The prompt to use for the authorization code request 176 | */ 177 | prompt?: 178 | | "select_account" 179 | | "consent" 180 | | "login" 181 | | "none" 182 | | "select_account consent"; 183 | /** 184 | * The response mode to use for the authorization code request 185 | */ 186 | responseMode?: "query" | "form_post"; 187 | /** 188 | * If enabled, the user info will be overridden with the provider user info 189 | * This is useful if you want to use the provider user info to update the user info 190 | * 191 | * @default false 192 | */ 193 | overrideUserInfoOnSignIn?: boolean; 194 | }; 195 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/facebook.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Facebook 3 | description: Facebook provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Facebook credentials 9 | To use Facebook sign in, you need a client ID and client Secret. You can get them from the [Facebook Developer Portal](https://developers.facebook.com/). 10 | Select your app, navigate to **App Settings > Basic**, locate the following: 11 | - **App ID**: This is your `clientId` 12 | - **App Secret**: This is your `clientSecret`. 13 | 14 | <Callout type="warn"> 15 | Avoid exposing the `clientSecret` in client-side code (e.g., frontend apps) because it’s sensitive information. 16 | </Callout> 17 | 18 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/facebook` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 19 | </Step> 20 | 21 | <Step> 22 | ### Configure the provider 23 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 24 | 25 | ```ts title="auth.ts" 26 | import { betterAuth } from "better-auth" 27 | 28 | export const auth = betterAuth({ 29 | socialProviders: { 30 | facebook: { // [!code highlight] 31 | clientId: process.env.FACEBOOK_CLIENT_ID as string, // [!code highlight] 32 | clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string, // [!code highlight] 33 | }, // [!code highlight] 34 | }, 35 | }) 36 | ``` 37 | 38 | <Callout> 39 | BetterAuth also supports Facebook Login for Business, all you need 40 | to do is provide the `configId` as listed in **Facebook Login For Business > Configurations** alongside your `clientId` and `clientSecret`. Note that the app must be a Business app and, since BetterAuth expects to have an email address and account id, the configuration must be of the "User access token" type. "System-user access token" is not supported. 41 | </Callout> 42 | </Step> 43 | <Step> 44 | ### Sign In with Facebook 45 | To sign in with Facebook, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 46 | - `provider`: The provider to use. It should be set to `facebook`. 47 | 48 | ```ts title="auth-client.ts" 49 | import { createAuthClient } from "better-auth/auth-client" 50 | const authClient = createAuthClient() 51 | 52 | const signIn = async () => { 53 | const data = await authClient.signIn.social({ 54 | provider: "facebook" 55 | }) 56 | } 57 | ``` 58 | </Step> 59 | </Steps> 60 | 61 | ## Additional Configuration 62 | 63 | ### Scopes 64 | By default, Facebook provides basic user information. If you need additional permissions, you can specify scopes in your auth configuration: 65 | 66 | ```ts title="auth.ts" 67 | export const auth = betterAuth({ 68 | socialProviders: { 69 | facebook: { 70 | clientId: process.env.FACEBOOK_CLIENT_ID as string, 71 | clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string, 72 | scopes: ["email", "public_profile", "user_friends"], // Overwrites permissions 73 | fields: ["user_friends"], // Extending list of fields 74 | }, 75 | }, 76 | }) 77 | ``` 78 | 79 | Additional options: 80 | - `scopes`: Access basic account information (overwrites). 81 | - Default: `"email", "public_profile"` 82 | - `fields`: Extend list of fields to retrieve from the Facebook user profile (assignment). 83 | - Default: `"id", "name", "email", "picture"` 84 | 85 | ### Sign In with Facebook With ID or Access Token 86 | 87 | To sign in with Facebook using the ID Token, you can use the `signIn.social` function to pass the ID Token. 88 | 89 | This is useful when you have the ID Token from Facebook on the client-side and want to use it to sign in on the server. 90 | 91 | <Callout> 92 | If ID token is provided no redirection will happen, and the user will be signed in directly. 93 | </Callout> 94 | 95 | For limited login, you need to pass `idToken.token`, for only `accessToken` you need to pass `idToken.accessToken` and `idToken.token` together because of (#1183)[https://github.com/better-auth/better-auth/issues/1183]. 96 | 97 | 98 | ```ts title="auth-client.ts" 99 | const data = await authClient.signIn.social({ 100 | provider: "facebook", 101 | idToken: { // [!code highlight] 102 | ...(platform === 'ios' ? // [!code highlight] 103 | { token: idToken } // [!code highlight] 104 | : { token: accessToken, accessToken: accessToken }), // [!code highlight] 105 | }, 106 | }) 107 | ``` 108 | 109 | For a complete list of available permissions, refer to the [Permissions Reference](https://developers.facebook.com/docs/permissions). 110 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/additional-fields/additional-fields.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { type Session } from "./../../types"; 2 | import { describe, expect, expectTypeOf, it } from "vitest"; 3 | import { getTestInstance } from "../../test-utils/test-instance"; 4 | import { createAuthClient } from "../../client"; 5 | import { inferAdditionalFields } from "./client"; 6 | import { twoFactor, twoFactorClient } from "../two-factor"; 7 | 8 | describe("additionalFields", async () => { 9 | const { auth, signInWithTestUser, customFetchImpl, sessionSetter } = 10 | await getTestInstance({ 11 | plugins: [twoFactor()], 12 | user: { 13 | additionalFields: { 14 | newField: { 15 | type: "string", 16 | defaultValue: "default-value", 17 | }, 18 | nonRequiredFiled: { 19 | type: "string", 20 | required: false, 21 | }, 22 | }, 23 | }, 24 | }); 25 | 26 | it("should extends fields", async () => { 27 | const { headers } = await signInWithTestUser(); 28 | const res = await auth.api.getSession({ 29 | headers, 30 | }); 31 | expect(res?.user.newField).toBeDefined(); 32 | expect(res?.user.nonRequiredFiled).toBeNull(); 33 | }); 34 | 35 | it("should require additional fields on signUp", async () => { 36 | await auth.api 37 | .signUpEmail({ 38 | body: { 39 | email: "[email protected]", 40 | name: "test", 41 | password: "test-password", 42 | newField: "new-field", 43 | nonRequiredFiled: "non-required-field", 44 | }, 45 | }) 46 | .catch(() => {}); 47 | 48 | const client = createAuthClient({ 49 | plugins: [ 50 | inferAdditionalFields({ 51 | user: { 52 | newField: { 53 | type: "string", 54 | }, 55 | nonRequiredFiled: { 56 | type: "string", 57 | defaultValue: "test", 58 | }, 59 | }, 60 | }), 61 | ], 62 | baseURL: "http://localhost:3000", 63 | fetchOptions: { 64 | customFetchImpl, 65 | }, 66 | }); 67 | const headers = new Headers(); 68 | await client.signUp.email( 69 | { 70 | email: "[email protected]", 71 | name: "test3", 72 | password: "test-password", 73 | newField: "new-field", 74 | }, 75 | { 76 | onSuccess: sessionSetter(headers), 77 | }, 78 | ); 79 | const res = await client.getSession({ 80 | fetchOptions: { 81 | headers, 82 | }, 83 | }); 84 | expect(res.data?.user.newField).toBe("new-field"); 85 | }); 86 | 87 | it("should infer additional fields on update", async () => { 88 | const client = createAuthClient({ 89 | plugins: [ 90 | inferAdditionalFields({ 91 | user: { 92 | newField: { 93 | type: "string", 94 | }, 95 | }, 96 | }), 97 | ], 98 | baseURL: "http://localhost:3000", 99 | fetchOptions: { 100 | customFetchImpl, 101 | }, 102 | }); 103 | const headers = new Headers(); 104 | await client.signUp.email( 105 | { 106 | email: "[email protected]", 107 | name: "test5", 108 | password: "test-password", 109 | newField: "new-field", 110 | }, 111 | { 112 | onSuccess: sessionSetter(headers), 113 | }, 114 | ); 115 | const res = await client.updateUser({ 116 | name: "test", 117 | newField: "updated-field", 118 | fetchOptions: { 119 | headers, 120 | }, 121 | }); 122 | const session = await client.getSession({ 123 | fetchOptions: { 124 | headers, 125 | throw: true, 126 | }, 127 | }); 128 | expect(session?.user.newField).toBe("updated-field"); 129 | }); 130 | 131 | it("should work with other plugins", async () => { 132 | const client = createAuthClient({ 133 | plugins: [ 134 | inferAdditionalFields({ 135 | user: { 136 | newField: { 137 | type: "string", 138 | required: true, 139 | }, 140 | }, 141 | }), 142 | twoFactorClient(), 143 | ], 144 | baseURL: "http://localhost:3000", 145 | fetchOptions: { 146 | customFetchImpl, 147 | }, 148 | }); 149 | expectTypeOf(client.twoFactor).toMatchTypeOf<{}>(); 150 | 151 | const headers = new Headers(); 152 | await client.signUp.email( 153 | { 154 | email: "[email protected]", 155 | name: "test4", 156 | password: "test-password", 157 | newField: "new-field", 158 | }, 159 | { 160 | onSuccess: sessionSetter(headers), 161 | }, 162 | ); 163 | const res = await client.updateUser( 164 | { 165 | name: "test", 166 | newField: "updated-field", 167 | }, 168 | { 169 | headers, 170 | }, 171 | ); 172 | }); 173 | 174 | it("should infer it on the client", async () => { 175 | const client = createAuthClient({ 176 | plugins: [inferAdditionalFields<typeof auth>()], 177 | }); 178 | type t = Awaited<ReturnType<typeof client.getSession>>["data"]; 179 | expectTypeOf<t>().toMatchTypeOf<{ 180 | user: { 181 | id: string; 182 | email: string; 183 | emailVerified: boolean; 184 | name: string; 185 | createdAt: Date; 186 | updatedAt: Date; 187 | image?: string | undefined; 188 | newField: string; 189 | nonRequiredFiled?: string | undefined; 190 | }; 191 | session: Session; 192 | } | null>; 193 | }); 194 | 195 | it("should infer it on the client without direct import", async () => { 196 | const client = createAuthClient({ 197 | plugins: [ 198 | inferAdditionalFields({ 199 | user: { 200 | newField: { 201 | type: "string", 202 | }, 203 | }, 204 | }), 205 | ], 206 | }); 207 | type t = Awaited<ReturnType<typeof client.getSession>>["data"]; 208 | expectTypeOf<t>().toMatchTypeOf<{ 209 | user: { 210 | id: string; 211 | email: string; 212 | emailVerified: boolean; 213 | name: string; 214 | createdAt: Date; 215 | updatedAt: Date; 216 | image?: string | undefined; 217 | newField: string; 218 | }; 219 | session: Session; 220 | } | null>; 221 | }); 222 | }); 223 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/captcha.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Captcha 3 | description: Captcha plugin 4 | --- 5 | 6 | The **Captcha Plugin** integrates bot protection into your Better Auth system by adding captcha verification for key endpoints. This plugin ensures that only human users can perform actions like signing up, signing in, or resetting passwords. The following providers are currently supported: 7 | - [Google reCAPTCHA](https://developers.google.com/recaptcha) 8 | - [Cloudflare Turnstile](https://www.cloudflare.com/application-services/products/turnstile/) 9 | - [hCaptcha](https://www.hcaptcha.com/) 10 | - [CaptchaFox](https://captchafox.com/) 11 | 12 | <Callout type="info"> 13 | This plugin works out of the box with <Link href="/docs/authentication/email-password">Email & Password</Link> authentication. To use it with other authentication methods, you will need to configure the <Link href="/docs/plugins/captcha#plugin-options">endpoints</Link> array in the plugin options. 14 | </Callout> 15 | 16 | ## Installation 17 | 18 | <Steps> 19 | <Step> 20 | ### Add the plugin to your **auth** config 21 | 22 | ```ts title="auth.ts" 23 | import { betterAuth } from "better-auth"; 24 | import { captcha } from "better-auth/plugins"; 25 | 26 | export const auth = betterAuth({ 27 | plugins: [ // [!code highlight] 28 | captcha({ // [!code highlight] 29 | provider: "cloudflare-turnstile", // or google-recaptcha, hcaptcha, captchafox // [!code highlight] 30 | secretKey: process.env.TURNSTILE_SECRET_KEY!, // [!code highlight] 31 | }), // [!code highlight] 32 | ], // [!code highlight] 33 | }); 34 | ``` 35 | 36 | </Step> 37 | <Step> 38 | ### Add the captcha token to your request headers 39 | 40 | Add the captcha token to your request headers for all protected endpoints. This example shows how to include it in a `signIn` request: 41 | 42 | ```ts 43 | await authClient.signIn.email({ 44 | email: "[email protected]", 45 | password: "secure-password", 46 | fetchOptions: { // [!code highlight] 47 | headers: { // [!code highlight] 48 | "x-captcha-response": turnstileToken, // [!code highlight] 49 | "x-captcha-user-remote-ip": userIp, // optional: forwards the user's IP address to the captcha service // [!code highlight] 50 | }, // [!code highlight] 51 | }, // [!code highlight] 52 | }); 53 | ``` 54 | 55 | - To implement Cloudflare Turnstile on the client side, follow the official [Cloudflare Turnstile documentation](https://developers.cloudflare.com/turnstile/) or use a library like [react-turnstile](https://www.npmjs.com/package/@marsidev/react-turnstile). 56 | - To implement Google reCAPTCHA on the client side, follow the official [Google reCAPTCHA documentation](https://developers.google.com/recaptcha/intro) or use libraries like [react-google-recaptcha](https://www.npmjs.com/package/react-google-recaptcha) (v2) and [react-google-recaptcha-v3](https://www.npmjs.com/package/react-google-recaptcha-v3) (v3). 57 | - To implement hCaptcha on the client side, follow the official [hCaptcha documentation](https://docs.hcaptcha.com/#add-the-hcaptcha-widget-to-your-webpage) or use libraries like [@hcaptcha/react-hcaptcha](https://www.npmjs.com/package/@hcaptcha/react-hcaptcha) 58 | - To implement CaptchaFox on the client side, follow the official [CaptchaFox documentation](https://docs.captchafox.com/getting-started) or use libraries like [@captchafox/react](https://www.npmjs.com/package/@captchafox/react) 59 | </Step> 60 | </Steps> 61 | 62 | ## How it works 63 | 64 | <Steps> 65 | <Step> 66 | The plugin acts as a middleware: it intercepts all `POST` requests to configured endpoints (see `endpoints` 67 | in the [Plugin Options](#plugin-options) section). 68 | </Step> 69 | <Step> 70 | it validates the captcha token on the server, by calling the captcha provider's `/siteverify`. 71 | </Step> 72 | <Step> 73 | - if the token is missing, gets rejected by the captcha provider, or if the `/siteverify` endpoint is 74 | unavailable, the plugin returns an error and interrupts the request. 75 | - if the token is accepted by the captcha provider, the middleware returns `undefined`, meaning the request is allowed to proceed. 76 | 77 | </Step> 78 | </Steps> 79 | 80 | ## Plugin Options 81 | 82 | - **`provider` (required)**: your captcha provider. 83 | - **`secretKey` (required)**: your provider's secret key used for the server-side validation. 84 | - `endpoints` (optional): overrides the default array of paths where captcha validation is enforced. Default is: `["/sign-up/email", "/sign-in/email", "/forget-password",]`. 85 | - `minScore` (optional - only *Google ReCAPTCHA v3*): minimum score threshold. Default is `0.5`. 86 | - `siteKey` (optional - only *hCaptcha* and *CaptchaFox*): prevents tokens issued on one sitekey from being redeemed elsewhere. 87 | - `siteVerifyURLOverride` (optional): overrides endpoint URL for the captcha verification request. ```