This is page 24 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-declaration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.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 │ │ └── 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.base.json ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import type { RateLimit } from "../../types"; 4 | 5 | describe( 6 | "rate-limiter", 7 | { 8 | timeout: 10000, 9 | }, 10 | async () => { 11 | const { client, testUser } = await getTestInstance({ 12 | rateLimit: { 13 | enabled: true, 14 | window: 10, 15 | max: 20, 16 | }, 17 | }); 18 | 19 | it("should return 429 after 3 request for sign-in", async () => { 20 | for (let i = 0; i < 5; i++) { 21 | const response = await client.signIn.email({ 22 | email: testUser.email, 23 | password: testUser.password, 24 | }); 25 | if (i >= 3) { 26 | expect(response.error?.status).toBe(429); 27 | } else { 28 | expect(response.error).toBeNull(); 29 | } 30 | } 31 | }); 32 | 33 | it("should reset the limit after the window period", async () => { 34 | vi.useFakeTimers(); 35 | vi.advanceTimersByTime(11000); 36 | for (let i = 0; i < 5; i++) { 37 | const res = await client.signIn.email({ 38 | email: testUser.email, 39 | password: testUser.password, 40 | }); 41 | if (i >= 3) { 42 | expect(res.error?.status).toBe(429); 43 | } else { 44 | expect(res.error).toBeNull(); 45 | } 46 | } 47 | }); 48 | 49 | it("should respond the correct retry-after header", async () => { 50 | vi.useFakeTimers(); 51 | vi.advanceTimersByTime(3000); 52 | let retryAfter = ""; 53 | await client.signIn.email( 54 | { 55 | email: testUser.email, 56 | password: testUser.password, 57 | }, 58 | { 59 | onError(context) { 60 | retryAfter = context.response.headers.get("X-Retry-After") ?? ""; 61 | }, 62 | }, 63 | ); 64 | expect(retryAfter).toBe("7"); 65 | }); 66 | 67 | it("should rate limit based on the path", async () => { 68 | const signInRes = await client.signIn.email({ 69 | email: testUser.email, 70 | password: testUser.password, 71 | }); 72 | expect(signInRes.error?.status).toBe(429); 73 | 74 | const signUpRes = await client.signUp.email({ 75 | email: "[email protected]", 76 | password: testUser.password, 77 | name: "test", 78 | }); 79 | expect(signUpRes.error).toBeNull(); 80 | }); 81 | 82 | it("non-special-rules limits", async () => { 83 | for (let i = 0; i < 25; i++) { 84 | const response = await client.getSession(); 85 | expect(response.error?.status).toBe(i >= 20 ? 429 : undefined); 86 | } 87 | }); 88 | 89 | it("query params should be ignored", async () => { 90 | for (let i = 0; i < 25; i++) { 91 | const response = await client.listSessions({ 92 | fetchOptions: { 93 | query: { 94 | "test-query": Math.random().toString(), 95 | }, 96 | }, 97 | }); 98 | 99 | if (i >= 20) { 100 | expect(response.error?.status).toBe(429); 101 | } else { 102 | expect(response.error?.status).toBe(401); 103 | } 104 | } 105 | }); 106 | }, 107 | ); 108 | 109 | describe("custom rate limiting storage", async () => { 110 | let store = new Map<string, string>(); 111 | const expirationMap = new Map<string, number>(); 112 | const { client, testUser } = await getTestInstance({ 113 | rateLimit: { 114 | enabled: true, 115 | }, 116 | secondaryStorage: { 117 | set(key, value, ttl) { 118 | store.set(key, value); 119 | if (ttl) expirationMap.set(key, ttl); 120 | }, 121 | get(key) { 122 | return store.get(key) || null; 123 | }, 124 | delete(key) { 125 | store.delete(key); 126 | expirationMap.delete(key); 127 | }, 128 | }, 129 | }); 130 | 131 | it("should use custom storage", async () => { 132 | await client.getSession(); 133 | expect(store.size).toBe(3); 134 | let lastRequest = Date.now(); 135 | for (let i = 0; i < 4; i++) { 136 | const response = await client.signIn.email({ 137 | email: testUser.email, 138 | password: testUser.password, 139 | }); 140 | const rateLimitData: RateLimit = JSON.parse( 141 | store.get("127.0.0.1/sign-in/email") ?? "{}", 142 | ); 143 | expect(rateLimitData.lastRequest).toBeGreaterThanOrEqual(lastRequest); 144 | lastRequest = rateLimitData.lastRequest; 145 | if (i >= 3) { 146 | expect(response.error?.status).toBe(429); 147 | expect(rateLimitData.count).toBe(3); 148 | } else { 149 | expect(response.error).toBeNull(); 150 | expect(rateLimitData.count).toBe(i + 1); 151 | } 152 | const rateLimitExp = expirationMap.get("127.0.0.1/sign-in/email"); 153 | expect(rateLimitExp).toBe(10); 154 | } 155 | }); 156 | }); 157 | 158 | describe("should work with custom rules", async () => { 159 | const { client, testUser } = await getTestInstance({ 160 | rateLimit: { 161 | enabled: true, 162 | storage: "database", 163 | customRules: { 164 | "/sign-in/*": { 165 | window: 10, 166 | max: 2, 167 | }, 168 | "/sign-up/email": { 169 | window: 10, 170 | max: 3, 171 | }, 172 | "/get-session": false, 173 | }, 174 | }, 175 | }); 176 | 177 | it("should use custom rules", async () => { 178 | for (let i = 0; i < 4; i++) { 179 | const response = await client.signIn.email({ 180 | email: testUser.email, 181 | password: testUser.password, 182 | }); 183 | if (i >= 2) { 184 | expect(response.error?.status).toBe(429); 185 | } else { 186 | expect(response.error).toBeNull(); 187 | } 188 | } 189 | 190 | for (let i = 0; i < 5; i++) { 191 | const response = await client.signUp.email({ 192 | email: `${Math.random()}@test.com`, 193 | password: testUser.password, 194 | name: "test", 195 | }); 196 | if (i >= 3) { 197 | expect(response.error?.status).toBe(429); 198 | } else { 199 | expect(response.error).toBeNull(); 200 | } 201 | } 202 | }); 203 | 204 | it("should use default rules if custom rules are not defined", async () => { 205 | for (let i = 0; i < 5; i++) { 206 | const response = await client.getSession(); 207 | if (i >= 20) { 208 | expect(response.error?.status).toBe(429); 209 | } else { 210 | expect(response.error).toBeNull(); 211 | } 212 | } 213 | }); 214 | 215 | it("should not rate limit if custom rule is false", async () => { 216 | let i = 0; 217 | let response = null; 218 | for (; i < 110; i++) { 219 | response = await client.getSession().then((res) => res.error); 220 | } 221 | expect(response).toBeNull(); 222 | expect(i).toBe(110); 223 | }); 224 | }); 225 | 226 | describe("should work in development/test environment", () => { 227 | const LOCALHOST_IP = "127.0.0.1"; 228 | const REQUEST_PATH = "/sign-in/email"; 229 | 230 | let originalNodeEnv: string | undefined; 231 | beforeEach(() => { 232 | originalNodeEnv = process.env.NODE_ENV; 233 | }); 234 | afterEach(() => { 235 | process.env.NODE_ENV = originalNodeEnv; 236 | vi.unstubAllEnvs(); 237 | }); 238 | 239 | it("should work in development environment", async () => { 240 | vi.stubEnv("NODE_ENV", "development"); 241 | 242 | const store = new Map<string, string>(); 243 | const { client, testUser } = await getTestInstance({ 244 | rateLimit: { 245 | enabled: true, 246 | window: 10, 247 | max: 3, 248 | }, 249 | secondaryStorage: { 250 | set(key, value) { 251 | store.set(key, value); 252 | }, 253 | get(key) { 254 | return store.get(key) || null; 255 | }, 256 | delete(key) { 257 | store.delete(key); 258 | }, 259 | }, 260 | }); 261 | 262 | for (let i = 0; i < 4; i++) { 263 | const response = await client.signIn.email({ 264 | email: testUser.email, 265 | password: testUser.password, 266 | }); 267 | 268 | if (i >= 3) { 269 | expect(response.error?.status).toBe(429); 270 | } else { 271 | expect(response.error).toBeNull(); 272 | } 273 | } 274 | 275 | const signInKeys = Array.from(store.keys()).filter((key) => 276 | key.endsWith(REQUEST_PATH), 277 | ); 278 | 279 | expect(signInKeys.length).toBeGreaterThan(0); 280 | expect(signInKeys[0]).toBe(`${LOCALHOST_IP}${REQUEST_PATH}`); 281 | }); 282 | 283 | it("should work in test environment", async () => { 284 | vi.stubEnv("NODE_ENV", "test"); 285 | 286 | const store = new Map<string, string>(); 287 | const { client, testUser } = await getTestInstance({ 288 | rateLimit: { 289 | enabled: true, 290 | window: 10, 291 | max: 3, 292 | }, 293 | secondaryStorage: { 294 | set(key, value) { 295 | store.set(key, value); 296 | }, 297 | get(key) { 298 | return store.get(key) || null; 299 | }, 300 | delete(key) { 301 | store.delete(key); 302 | }, 303 | }, 304 | }); 305 | 306 | for (let i = 0; i < 4; i++) { 307 | const response = await client.signIn.email({ 308 | email: testUser.email, 309 | password: testUser.password, 310 | }); 311 | 312 | if (i >= 3) { 313 | expect(response.error?.status).toBe(429); 314 | } else { 315 | expect(response.error).toBeNull(); 316 | } 317 | } 318 | 319 | const signInKeys = Array.from(store.keys()).filter((key) => 320 | key.endsWith(REQUEST_PATH), 321 | ); 322 | 323 | expect(signInKeys.length).toBeGreaterThan(0); 324 | expect(signInKeys[0]).toBe(`${LOCALHOST_IP}${REQUEST_PATH}`); 325 | }); 326 | }); 327 | ``` -------------------------------------------------------------------------------- /docs/app/global.css: -------------------------------------------------------------------------------- ```css 1 | @import "tailwindcss"; 2 | @import "fumadocs-ui/css/black.css"; 3 | @import "fumadocs-ui/css/preset.css"; 4 | @config "../tailwind.config.js"; 5 | @plugin 'tailwindcss-animate'; 6 | @custom-variant dark (&:is(.dark *)); 7 | 8 | :root { 9 | --fd-nav-height: 56px; 10 | --fd-banner-height: 0px; 11 | --fd-tocnav-height: 0px; 12 | 13 | --background: oklch(1 0 0); 14 | 15 | --foreground: oklch(0.147 0.004 49.25); 16 | 17 | --card: oklch(1 0 0); 18 | 19 | --card-foreground: oklch(0.147 0.004 49.25); 20 | 21 | --popover: oklch(1 0 0); 22 | 23 | --popover-foreground: oklch(0.147 0.004 49.25); 24 | 25 | --primary: oklch(0.216 0.006 56.043); 26 | 27 | --primary-foreground: oklch(0.985 0.001 106.423); 28 | 29 | --secondary: oklch(0.97 0.001 106.424); 30 | 31 | --secondary-foreground: oklch(0.216 0.006 56.043); 32 | 33 | --muted: oklch(0.97 0.001 106.424); 34 | 35 | --muted-foreground: oklch(0.553 0.013 58.071); 36 | 37 | --accent: oklch(0.97 0.001 106.424); 38 | 39 | --accent-foreground: oklch(0.216 0.006 56.043); 40 | 41 | --destructive: oklch(0.577 0.245 27.325); 42 | 43 | --destructive-foreground: oklch(0.577 0.245 27.325); 44 | 45 | --border: oklch(0.923 0.003 48.717); 46 | 47 | --input: oklch(0.923 0.003 48.717); 48 | 49 | --ring: oklch(0.709 0.01 56.259); 50 | 51 | --chart-1: oklch(0.646 0.222 41.116); 52 | 53 | --chart-2: oklch(0.6 0.118 184.704); 54 | 55 | --chart-3: oklch(0.398 0.07 227.392); 56 | 57 | --chart-4: oklch(0.828 0.189 84.429); 58 | 59 | --chart-5: oklch(0.769 0.188 70.08); 60 | 61 | --radius: 0.2rem; 62 | 63 | --sidebar: oklch(0.985 0.001 106.423); 64 | 65 | --sidebar-foreground: oklch(0.147 0.004 49.25); 66 | 67 | --sidebar-primary: oklch(0.216 0.006 56.043); 68 | 69 | --sidebar-primary-foreground: oklch(0.985 0.001 106.423); 70 | 71 | --sidebar-accent: oklch(0.97 0.001 106.424); 72 | 73 | --sidebar-accent-foreground: oklch(0.216 0.006 56.043); 74 | 75 | --sidebar-border: oklch(0.923 0.003 48.717); 76 | 77 | --sidebar-ring: oklch(0.709 0.01 56.259); 78 | 79 | /* Scrollbar theme (light) */ 80 | --scrollbar-thumb: var(--border); 81 | --scrollbar-thumb-hover: var(--ring); 82 | --scrollbar-track: transparent; 83 | } 84 | 85 | .dark { 86 | --background: hsl(0 0% 0%); 87 | 88 | --foreground: oklch(0.985 0.001 106.423); 89 | 90 | --card: oklch(0.147 0.004 49.25); 91 | 92 | --card-foreground: oklch(0.985 0.001 106.423); 93 | 94 | --popover: oklch(0.147 0.004 49.25); 95 | 96 | --popover-foreground: oklch(0.985 0.001 106.423); 97 | 98 | --primary: oklch(0.985 0.001 106.423); 99 | 100 | --primary-foreground: oklch(0.216 0.006 56.043); 101 | 102 | --secondary: oklch(0.268 0.007 34.298); 103 | 104 | --secondary-foreground: oklch(0.985 0.001 106.423); 105 | 106 | --muted: oklch(0.268 0.007 34.298); 107 | 108 | --muted-foreground: oklch(0.709 0.01 56.259); 109 | 110 | --accent: oklch(0.268 0.007 34.298); 111 | 112 | --accent-foreground: oklch(0.985 0.001 106.423); 113 | 114 | --destructive: oklch(0.396 0.141 25.723); 115 | 116 | --destructive-foreground: oklch(0.637 0.237 25.331); 117 | 118 | --border: oklch(0.268 0.007 34.298); 119 | 120 | --input: oklch(0.268 0.007 34.298); 121 | 122 | --ring: oklch(0.553 0.013 58.071); 123 | 124 | --chart-1: oklch(0.488 0.243 264.376); 125 | 126 | --chart-2: oklch(0.696 0.17 162.48); 127 | 128 | --chart-3: oklch(0.769 0.188 70.08); 129 | 130 | --chart-4: oklch(0.627 0.265 303.9); 131 | 132 | --chart-5: oklch(0.645 0.246 16.439); 133 | 134 | --sidebar: oklch(0.216 0.006 56.043); 135 | 136 | --sidebar-foreground: oklch(0.985 0.001 106.423); 137 | 138 | --sidebar-primary: oklch(0.488 0.243 264.376); 139 | 140 | --sidebar-primary-foreground: oklch(0.985 0.001 106.423); 141 | 142 | --sidebar-accent: oklch(0.268 0.007 34.298); 143 | 144 | --sidebar-accent-foreground: oklch(0.985 0.001 106.423); 145 | 146 | --sidebar-border: oklch(0.268 0.007 34.298); 147 | 148 | --sidebar-ring: oklch(0.553 0.013 58.071); 149 | 150 | /* Scrollbar theme (dark) */ 151 | --scrollbar-thumb: var(--border); 152 | --scrollbar-thumb-hover: var(--ring); 153 | --scrollbar-track: transparent; 154 | } 155 | 156 | @theme inline { 157 | --color-background: var(--background); 158 | 159 | --color-foreground: var(--foreground); 160 | 161 | --color-card: var(--card); 162 | 163 | --color-card-foreground: var(--card-foreground); 164 | 165 | --color-popover: var(--popover); 166 | 167 | --color-popover-foreground: var(--popover-foreground); 168 | 169 | --color-primary: var(--primary); 170 | 171 | --color-primary-foreground: var(--primary-foreground); 172 | 173 | --color-secondary: var(--secondary); 174 | 175 | --color-secondary-foreground: var(--secondary-foreground); 176 | 177 | --color-muted: var(--muted); 178 | 179 | --color-muted-foreground: var(--muted-foreground); 180 | 181 | --color-accent: var(--accent); 182 | 183 | --color-accent-foreground: var(--accent-foreground); 184 | 185 | --color-destructive: var(--destructive); 186 | 187 | --color-destructive-foreground: var(--destructive-foreground); 188 | 189 | --color-border: var(--border); 190 | 191 | --color-input: var(--input); 192 | 193 | --color-ring: var(--ring); 194 | 195 | --color-chart-1: var(--chart-1); 196 | 197 | --color-chart-2: var(--chart-2); 198 | 199 | --color-chart-3: var(--chart-3); 200 | 201 | --color-chart-4: var(--chart-4); 202 | 203 | --color-chart-5: var(--chart-5); 204 | 205 | --radius-sm: calc(var(--radius) - 4px); 206 | 207 | --radius-md: calc(var(--radius) - 2px); 208 | 209 | --radius-lg: var(--radius); 210 | 211 | --radius-xl: calc(var(--radius) + 4px); 212 | 213 | --color-sidebar: var(--sidebar); 214 | 215 | --color-sidebar-foreground: var(--sidebar-foreground); 216 | 217 | --color-sidebar-primary: var(--sidebar-primary); 218 | 219 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 220 | 221 | --color-sidebar-accent: var(--sidebar-accent); 222 | 223 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 224 | 225 | --color-sidebar-border: var(--sidebar-border); 226 | 227 | --color-sidebar-ring: var(--sidebar-ring); 228 | --animate-accordion-down: accordion-down 0.2s ease-out; 229 | --animate-accordion-up: accordion-up 0.2s ease-out; 230 | 231 | @keyframes accordion-down { 232 | from { 233 | height: 0; 234 | } 235 | to { 236 | height: var(--radix-accordion-content-height); 237 | } 238 | } 239 | 240 | @keyframes accordion-up { 241 | from { 242 | height: var(--radix-accordion-content-height); 243 | } 244 | to { 245 | height: 0; 246 | } 247 | } 248 | } 249 | 250 | @layer base { 251 | * { 252 | @apply border-border outline-ring/50; 253 | } 254 | body { 255 | @apply overscroll-none bg-background text-foreground selection:bg-foreground selection:text-background; 256 | } 257 | } 258 | 259 | html { 260 | scroll-behavior: auto; 261 | scroll-padding-top: calc( 262 | var(--fd-nav-height, 56px) + 263 | var(--fd-banner-height, 0px) + 264 | var(--fd-tocnav-height, 0px) + 265 | 24px 266 | ); 267 | } 268 | 269 | html:not([data-anchor-scrolling]) { 270 | scroll-behavior: smooth; 271 | } 272 | 273 | /* Global, accessible custom scrollbars */ 274 | * { 275 | scrollbar-width: thin; /* Firefox */ 276 | scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); 277 | } 278 | 279 | /* WebKit-based browsers */ 280 | ::-webkit-scrollbar { 281 | width: 12px; 282 | height: 12px; 283 | } 284 | ::-webkit-scrollbar-track { 285 | background: var(--scrollbar-track); 286 | } 287 | ::-webkit-scrollbar-thumb { 288 | background-color: var(--scrollbar-thumb); 289 | border-radius: 9999px; 290 | border: 3px solid transparent; /* creates a gap between thumb and track */ 291 | background-clip: content-box; 292 | } 293 | ::-webkit-scrollbar-thumb:hover { 294 | background-color: var(--scrollbar-thumb-hover); 295 | } 296 | ::-webkit-scrollbar-corner { 297 | background: transparent; 298 | } 299 | 300 | @layer utilities { 301 | .no-scrollbar::-webkit-scrollbar { 302 | display: none; 303 | } 304 | .no-scrollbar { 305 | -ms-overflow-style: none; 306 | scrollbar-width: none; 307 | } 308 | } 309 | 310 | .markdown-content { 311 | @apply text-sm leading-relaxed; 312 | } 313 | 314 | .markdown-content pre { 315 | @apply max-w-full overflow-x-auto; 316 | } 317 | 318 | .markdown-content pre code { 319 | @apply whitespace-pre-wrap break-words; 320 | } 321 | 322 | .markdown-content h1, 323 | .markdown-content h2, 324 | .markdown-content h3, 325 | .markdown-content h4, 326 | .markdown-content h5, 327 | .markdown-content h6 { 328 | @apply font-semibold text-foreground; 329 | } 330 | 331 | .markdown-content p { 332 | @apply mb-2 last:mb-0; 333 | } 334 | 335 | .markdown-content ul, 336 | .markdown-content ol { 337 | @apply space-y-2 list-disc; 338 | } 339 | 340 | .markdown-content li { 341 | @apply text-sm; 342 | } 343 | 344 | .markdown-content code { 345 | @apply bg-muted px-1.5 py-0.5 rounded text-xs font-mono; 346 | } 347 | 348 | .markdown-content pre { 349 | @apply overflow-x-auto; 350 | } 351 | 352 | .markdown-content blockquote { 353 | @apply border-l-4 border-muted-foreground/20 pl-4 my-2 italic; 354 | } 355 | 356 | .markdown-content table { 357 | @apply w-full border-collapse; 358 | } 359 | 360 | .markdown-content th, 361 | .markdown-content td { 362 | @apply border border-border px-2 py-1 text-xs; 363 | } 364 | 365 | .markdown-content th { 366 | @apply bg-muted font-medium; 367 | } 368 | 369 | @keyframes stream-pulse { 370 | 0%, 371 | 100% { 372 | opacity: 1; 373 | } 374 | 50% { 375 | opacity: 0.5; 376 | } 377 | } 378 | 379 | .streaming-cursor { 380 | animation: stream-pulse 1s ease-in-out infinite; 381 | } 382 | ``` -------------------------------------------------------------------------------- /docs/components/ui/context-menu.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; 5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function ContextMenu({ 10 | ...props 11 | }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { 12 | return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />; 13 | } 14 | 15 | function ContextMenuTrigger({ 16 | ...props 17 | }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { 18 | return ( 19 | <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> 20 | ); 21 | } 22 | 23 | function ContextMenuGroup({ 24 | ...props 25 | }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { 26 | return ( 27 | <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> 28 | ); 29 | } 30 | 31 | function ContextMenuPortal({ 32 | ...props 33 | }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { 34 | return ( 35 | <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> 36 | ); 37 | } 38 | 39 | function ContextMenuSub({ 40 | ...props 41 | }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { 42 | return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />; 43 | } 44 | 45 | function ContextMenuRadioGroup({ 46 | ...props 47 | }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) { 48 | return ( 49 | <ContextMenuPrimitive.RadioGroup 50 | data-slot="context-menu-radio-group" 51 | {...props} 52 | /> 53 | ); 54 | } 55 | 56 | function ContextMenuSubTrigger({ 57 | className, 58 | inset, 59 | children, 60 | ...props 61 | }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { 62 | inset?: boolean; 63 | }) { 64 | return ( 65 | <ContextMenuPrimitive.SubTrigger 66 | data-slot="context-menu-sub-trigger" 67 | data-inset={inset} 68 | className={cn( 69 | "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 70 | className, 71 | )} 72 | {...props} 73 | > 74 | {children} 75 | <ChevronRightIcon className="ml-auto" /> 76 | </ContextMenuPrimitive.SubTrigger> 77 | ); 78 | } 79 | 80 | function ContextMenuSubContent({ 81 | className, 82 | ...props 83 | }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { 84 | return ( 85 | <ContextMenuPrimitive.SubContent 86 | data-slot="context-menu-sub-content" 87 | className={cn( 88 | "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg", 89 | className, 90 | )} 91 | {...props} 92 | /> 93 | ); 94 | } 95 | 96 | function ContextMenuContent({ 97 | className, 98 | ...props 99 | }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) { 100 | return ( 101 | <ContextMenuPrimitive.Portal> 102 | <ContextMenuPrimitive.Content 103 | data-slot="context-menu-content" 104 | className={cn( 105 | "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md", 106 | className, 107 | )} 108 | {...props} 109 | /> 110 | </ContextMenuPrimitive.Portal> 111 | ); 112 | } 113 | 114 | function ContextMenuItem({ 115 | className, 116 | inset, 117 | variant = "default", 118 | ...props 119 | }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { 120 | inset?: boolean; 121 | variant?: "default" | "destructive"; 122 | }) { 123 | return ( 124 | <ContextMenuPrimitive.Item 125 | data-slot="context-menu-item" 126 | data-inset={inset} 127 | data-variant={variant} 128 | className={cn( 129 | "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-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]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 130 | className, 131 | )} 132 | {...props} 133 | /> 134 | ); 135 | } 136 | 137 | function ContextMenuCheckboxItem({ 138 | className, 139 | children, 140 | checked, 141 | ...props 142 | }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) { 143 | return ( 144 | <ContextMenuPrimitive.CheckboxItem 145 | data-slot="context-menu-checkbox-item" 146 | className={cn( 147 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 148 | className, 149 | )} 150 | checked={checked} 151 | {...props} 152 | > 153 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 154 | <ContextMenuPrimitive.ItemIndicator> 155 | <CheckIcon className="size-4" /> 156 | </ContextMenuPrimitive.ItemIndicator> 157 | </span> 158 | {children} 159 | </ContextMenuPrimitive.CheckboxItem> 160 | ); 161 | } 162 | 163 | function ContextMenuRadioItem({ 164 | className, 165 | children, 166 | ...props 167 | }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) { 168 | return ( 169 | <ContextMenuPrimitive.RadioItem 170 | data-slot="context-menu-radio-item" 171 | className={cn( 172 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 173 | className, 174 | )} 175 | {...props} 176 | > 177 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 178 | <ContextMenuPrimitive.ItemIndicator> 179 | <CircleIcon className="size-2 fill-current" /> 180 | </ContextMenuPrimitive.ItemIndicator> 181 | </span> 182 | {children} 183 | </ContextMenuPrimitive.RadioItem> 184 | ); 185 | } 186 | 187 | function ContextMenuLabel({ 188 | className, 189 | inset, 190 | ...props 191 | }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { 192 | inset?: boolean; 193 | }) { 194 | return ( 195 | <ContextMenuPrimitive.Label 196 | data-slot="context-menu-label" 197 | data-inset={inset} 198 | className={cn( 199 | "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", 200 | className, 201 | )} 202 | {...props} 203 | /> 204 | ); 205 | } 206 | 207 | function ContextMenuSeparator({ 208 | className, 209 | ...props 210 | }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) { 211 | return ( 212 | <ContextMenuPrimitive.Separator 213 | data-slot="context-menu-separator" 214 | className={cn("bg-border -mx-1 my-1 h-px", className)} 215 | {...props} 216 | /> 217 | ); 218 | } 219 | 220 | function ContextMenuShortcut({ 221 | className, 222 | ...props 223 | }: React.ComponentProps<"span">) { 224 | return ( 225 | <span 226 | data-slot="context-menu-shortcut" 227 | className={cn( 228 | "text-muted-foreground ml-auto text-xs tracking-widest", 229 | className, 230 | )} 231 | {...props} 232 | /> 233 | ); 234 | } 235 | 236 | export { 237 | ContextMenu, 238 | ContextMenuTrigger, 239 | ContextMenuContent, 240 | ContextMenuItem, 241 | ContextMenuCheckboxItem, 242 | ContextMenuRadioItem, 243 | ContextMenuLabel, 244 | ContextMenuSeparator, 245 | ContextMenuShortcut, 246 | ContextMenuGroup, 247 | ContextMenuPortal, 248 | ContextMenuSub, 249 | ContextMenuSubContent, 250 | ContextMenuSubTrigger, 251 | ContextMenuRadioGroup, 252 | }; 253 | ``` -------------------------------------------------------------------------------- /docs/content/docs/guides/your-first-plugin.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Create your first plugin 3 | description: A step-by-step guide to creating your first Better Auth plugin. 4 | --- 5 | 6 | In this guide, we’ll walk you through the steps of creating your first Better Auth plugin. 7 | 8 | 9 | <Callout type="warn"> 10 | This guide assumes you have <Link href="/docs/installation">setup the basics</Link> of Better Auth and are ready to create your first plugin. 11 | </Callout> 12 | 13 | <Steps> 14 | <Step> 15 | ## Plan your idea 16 | Before beginning, you must know what plugin you intend to create. 17 | 18 | In this guide, we’ll create a **birthday plugin** to keep track of user birth dates. 19 | </Step> 20 | 21 | <Step> 22 | ## Server plugin first 23 | Better Auth plugins operate as a pair: a <Link href="/docs/concepts/plugins#create-a-server-plugin">server plugin</Link> and a <Link href="/docs/concepts/plugins#create-a-client-plugin">client plugin</Link>. 24 | The server plugin forms the foundation of your authentication system, while the client plugin provides convenient frontend APIs to interact with your server implementation. 25 | 26 | 27 | <Callout> 28 | You can read more about server/client plugins in our <Link href="/docs/concepts/plugins#creating-a-plugin">documentation</Link>. 29 | </Callout> 30 | 31 | ### Creating the server plugin 32 | Go ahead and find a suitable location to create your birthday plugin folder, with an `index.ts` file within. 33 | <Files> 34 | <Folder name="birthday-plugin" defaultOpen> 35 | <File name="index.ts" /> 36 | </Folder> 37 | </Files> 38 | In the `index.ts` file, we’ll export a function that represents our server plugin. 39 | This will be what we will later add to our plugin list in the `auth.ts` file. 40 | 41 | ```ts title="index.ts" 42 | import { createAuthClient } from "better-auth/client"; 43 | import type { BetterAuthPlugin } from "better-auth"; 44 | 45 | export const birthdayPlugin = () => 46 | ({ 47 | id: "birthdayPlugin", 48 | } satisfies BetterAuthPlugin); 49 | 50 | ``` 51 | Although this does nothing, you have technically just made yourself your first plugin, congratulations! 🎉 52 | 53 | </Step> 54 | 55 | <Step> 56 | ### Defining a schema 57 | In order to save each user’s birthday data, we must create a schema on top of the `user` model. 58 | 59 | By creating a schema here, this also allows <Link href="/docs/concepts/cli">Better Auth’s CLI</Link> to generate the schemas required to update your database. 60 | 61 | <Callout type="info"> 62 | You can learn more about <Link href="/docs/concepts/plugins#schema">plugin schemas here</Link>. 63 | </Callout> 64 | 65 | ```ts title="index.ts" 66 | //... 67 | export const birthdayPlugin = () => 68 | ({ 69 | id: "birthdayPlugin", 70 | schema: {// [!code highlight] 71 | user: {// [!code highlight] 72 | fields: {// [!code highlight] 73 | birthday: {// [!code highlight] 74 | type: "date", // string, number, boolean, date // [!code highlight] 75 | required: true, // if the field should be required on a new record. (default: false) // [!code highlight] 76 | unique: false, // if the field should be unique. (default: false) // [!code highlight] 77 | references: null // if the field is a reference to another table. (default: null) // [!code highlight] 78 | },// [!code highlight] 79 | },// [!code highlight] 80 | },// [!code highlight] 81 | }, 82 | } satisfies BetterAuthPlugin); 83 | ``` 84 | 85 | </Step> 86 | 87 | <Step> 88 | ### Authorization logic 89 | For this example guide, we’ll set up authentication logic to check and ensure that the user who signs-up is older than 5. 90 | But the same concept could be applied for something like verifying users agreeing to the TOS or anything alike. 91 | 92 | To do this, we’ll utilize <Link href="/docs/concepts/plugins#hooks">Hooks</Link>, which allows us to run code `before` or `after` an action is performed. 93 | 94 | ```ts title="index.ts" 95 | export const birthdayPlugin = () => ({ 96 | //... 97 | // In our case, we want to write authorization logic, 98 | // meaning we want to intercept it `before` hand. 99 | hooks: { 100 | before: [ 101 | { 102 | matcher: (context) => /* ... */, 103 | handler: createAuthMiddleware(async (ctx) => { 104 | //... 105 | }), 106 | }, 107 | ], 108 | }, 109 | } satisfies BetterAuthPlugin) 110 | ``` 111 | 112 | In our case we want to match any requests going to the signup path: 113 | ```ts title="Before hook" 114 | { 115 | matcher: (context) => context.path.startsWith("/sign-up/email"), 116 | //... 117 | } 118 | ``` 119 | 120 | And for our logic, we’ll write the following code to check the if user’s birthday makes them above 5 years old. 121 | ```ts title="Imports" 122 | import { APIError } from "better-auth/api"; 123 | import { createAuthMiddleware } from "better-auth/plugins"; 124 | ``` 125 | ```ts title="Before hook" 126 | { 127 | //... 128 | handler: createAuthMiddleware(async (ctx) => { 129 | const { birthday } = ctx.body; 130 | if(!(birthday instanceof Date)) { 131 | throw new APIError("BAD_REQUEST", { message: "Birthday must be of type Date." }); 132 | } 133 | 134 | const today = new Date(); 135 | const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5)); 136 | 137 | if(birthday >= fiveYearsAgo) { 138 | throw new APIError("BAD_REQUEST", { message: "User must be above 5 years old." }); 139 | } 140 | 141 | return { context: ctx }; 142 | }), 143 | } 144 | ``` 145 | 146 | **Authorized!** 🔒 147 | 148 | We’ve now successfully written code to ensure authorization for users above 5! 149 | 150 | </Step> 151 | 152 | <Step> 153 | ## Client Plugin 154 | We’re close to the finish line! 🏁 155 | 156 | Now that we have created our server plugin, the next step is to develop our client plugin. 157 | Since there isn’t much frontend APIs going on for this plugin, there isn’t much to do! 158 | 159 | First, let’s create our `client.ts` file first: 160 | <Files> 161 | <Folder name="birthday-plugin" defaultOpen> 162 | <File name="index.ts" /> 163 | <File name="client.ts" /> 164 | </Folder> 165 | </Files> 166 | Then, add the following code: 167 | ```ts title="client.ts" 168 | import { BetterAuthClientPlugin } from "better-auth"; 169 | import type { birthdayPlugin } from "./index"; // make sure to import the server plugin as a type // [!code highlight] 170 | 171 | type BirthdayPlugin = typeof birthdayPlugin; 172 | 173 | export const birthdayClientPlugin = () => { 174 | return { 175 | id: "birthdayPlugin", 176 | $InferServerPlugin: {} as ReturnType<BirthdayPlugin>, 177 | } satisfies BetterAuthClientPlugin; 178 | }; 179 | ``` 180 | What we’ve done is allow the client plugin to infer the types defined by our schema from the server plugin. 181 | 182 | And that’s it! This is all it takes for the birthday client plugin. 🎂 183 | 184 | </Step> 185 | 186 | <Step> 187 | ## Initiate your plugin! 188 | Both the `client` and `server` plugins are now ready, the last step is to import them to both your `auth-client.ts` and your `server.ts` files respectively to initiate the plugin. 189 | 190 | ### Server initiation 191 | ```ts title="server.ts" 192 | import { betterAuth } from "better-auth"; 193 | import { birthdayPlugin } from "./birthday-plugin";// [!code highlight] 194 | 195 | export const auth = betterAuth({ 196 | plugins: [ 197 | birthdayPlugin(),// [!code highlight] 198 | ] 199 | }); 200 | ``` 201 | 202 | ### Client initiation 203 | ```ts title="auth-client.ts" 204 | import { createAuthClient } from "better-auth/client"; 205 | import { birthdayClientPlugin } from "./birthday-plugin/client";// [!code highlight] 206 | 207 | const authClient = createAuthClient({ 208 | plugins: [ 209 | birthdayClientPlugin()// [!code highlight] 210 | ] 211 | }); 212 | ``` 213 | 214 | ### Oh yeah, the schemas! 215 | Don’t forget to add your `birthday` field to your `user` table model! 216 | 217 | Or, use the `generate` <Link href="/docs/concepts/cli#generate">CLI command</Link>: 218 | ```bash 219 | npx @better-auth/cli@latest generate 220 | ``` 221 | 222 | </Step> 223 | </Steps> 224 | 225 | ## Wrapping Up 226 | 227 | Congratulations! You’ve successfully created your first ever Better Auth plugin. 228 | We highly recommend you visit our <Link href="/docs/concepts/plugins">plugins documentation</Link> to learn more information. 229 | 230 | If you have a plugin you’d like to share with the community, feel free to let us know through 231 | our <Link href="https://discord.gg/better-auth">Discord server</Link>, 232 | or through a <Link href="https://github.com/better-auth/better-auth/pulls">pull-request</Link> 233 | and we may add it to the <Link href="/docs/plugins/community-plugins">community-plugins</Link> list! 234 | ``` -------------------------------------------------------------------------------- /packages/cli/test/generate-all-db.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { generateDrizzleSchema } from "../src/generators/drizzle"; 3 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 4 | import { twoFactor, username } from "better-auth/plugins"; 5 | import { passkey } from "better-auth/plugins/passkey"; 6 | import type { BetterAuthOptions } from "better-auth"; 7 | 8 | describe("generate drizzle schema for all databases", async () => { 9 | it("should generate drizzle schema for MySQL", async () => { 10 | const schema = await generateDrizzleSchema({ 11 | file: "test.drizzle", 12 | adapter: drizzleAdapter( 13 | {}, 14 | { 15 | provider: "mysql", 16 | schema: {}, 17 | }, 18 | )({} as BetterAuthOptions), 19 | options: { 20 | database: drizzleAdapter( 21 | {}, 22 | { 23 | provider: "mysql", 24 | schema: {}, 25 | }, 26 | ), 27 | plugins: [twoFactor(), username()], 28 | user: { 29 | modelName: "custom_user", 30 | }, 31 | account: { 32 | modelName: "custom_account", 33 | }, 34 | session: { 35 | modelName: "custom_session", 36 | }, 37 | verification: { 38 | modelName: "custom_verification", 39 | }, 40 | }, 41 | }); 42 | expect(schema.code).toMatchFileSnapshot( 43 | "./__snapshots__/auth-schema-mysql.txt", 44 | ); 45 | }); 46 | 47 | it("should generate drizzle schema for SQLite", async () => { 48 | const schema = await generateDrizzleSchema({ 49 | file: "test.drizzle", 50 | adapter: drizzleAdapter( 51 | {}, 52 | { 53 | provider: "sqlite", 54 | schema: {}, 55 | }, 56 | )({} as BetterAuthOptions), 57 | options: { 58 | database: drizzleAdapter( 59 | {}, 60 | { 61 | provider: "sqlite", 62 | schema: {}, 63 | }, 64 | ), 65 | plugins: [twoFactor(), username()], 66 | user: { 67 | modelName: "custom_user", 68 | }, 69 | account: { 70 | modelName: "custom_account", 71 | }, 72 | session: { 73 | modelName: "custom_session", 74 | }, 75 | verification: { 76 | modelName: "custom_verification", 77 | }, 78 | }, 79 | }); 80 | expect(schema.code).toMatchFileSnapshot( 81 | "./__snapshots__/auth-schema-sqlite.txt", 82 | ); 83 | }); 84 | 85 | it("should generate drizzle schema for MySQL with number id", async () => { 86 | const schema = await generateDrizzleSchema({ 87 | file: "test.drizzle", 88 | adapter: drizzleAdapter( 89 | {}, 90 | { 91 | provider: "mysql", 92 | schema: {}, 93 | }, 94 | )({} as BetterAuthOptions), 95 | options: { 96 | database: drizzleAdapter( 97 | {}, 98 | { 99 | provider: "mysql", 100 | schema: {}, 101 | }, 102 | ), 103 | plugins: [twoFactor(), username()], 104 | advanced: { 105 | database: { 106 | useNumberId: true, 107 | }, 108 | }, 109 | user: { 110 | modelName: "custom_user", 111 | }, 112 | account: { 113 | modelName: "custom_account", 114 | }, 115 | session: { 116 | modelName: "custom_session", 117 | }, 118 | verification: { 119 | modelName: "custom_verification", 120 | }, 121 | }, 122 | }); 123 | expect(schema.code).toMatchFileSnapshot( 124 | "./__snapshots__/auth-schema-mysql-number-id.txt", 125 | ); 126 | }); 127 | 128 | it("should generate drizzle schema for SQLite with number id", async () => { 129 | const schema = await generateDrizzleSchema({ 130 | file: "test.drizzle", 131 | adapter: drizzleAdapter( 132 | {}, 133 | { 134 | provider: "sqlite", 135 | schema: {}, 136 | }, 137 | )({} as BetterAuthOptions), 138 | options: { 139 | database: drizzleAdapter( 140 | {}, 141 | { 142 | provider: "sqlite", 143 | schema: {}, 144 | }, 145 | ), 146 | plugins: [twoFactor(), username()], 147 | advanced: { 148 | database: { 149 | useNumberId: true, 150 | }, 151 | }, 152 | user: { 153 | modelName: "custom_user", 154 | }, 155 | account: { 156 | modelName: "custom_account", 157 | }, 158 | session: { 159 | modelName: "custom_session", 160 | }, 161 | verification: { 162 | modelName: "custom_verification", 163 | }, 164 | }, 165 | }); 166 | expect(schema.code).toMatchFileSnapshot( 167 | "./__snapshots__/auth-schema-sqlite-number-id.txt", 168 | ); 169 | }); 170 | }); 171 | 172 | describe("generate drizzle schema for all databases with passkey plugin", async () => { 173 | it("should generate drizzle schema for MySQL with passkey plugin", async () => { 174 | const schema = await generateDrizzleSchema({ 175 | file: "test.drizzle", 176 | adapter: drizzleAdapter( 177 | {}, 178 | { 179 | provider: "mysql", 180 | schema: {}, 181 | }, 182 | )({} as BetterAuthOptions), 183 | options: { 184 | database: drizzleAdapter( 185 | {}, 186 | { 187 | provider: "mysql", 188 | schema: {}, 189 | }, 190 | ), 191 | plugins: [passkey()], 192 | user: { 193 | modelName: "custom_user", 194 | }, 195 | account: { 196 | modelName: "custom_account", 197 | }, 198 | session: { 199 | modelName: "custom_session", 200 | }, 201 | verification: { 202 | modelName: "custom_verification", 203 | }, 204 | }, 205 | }); 206 | expect(schema.code).toMatchFileSnapshot( 207 | "./__snapshots__/auth-schema-mysql-passkey.txt", 208 | ); 209 | }); 210 | 211 | it("should generate drizzle schema for SQLite with passkey plugin", async () => { 212 | const schema = await generateDrizzleSchema({ 213 | file: "test.drizzle", 214 | adapter: drizzleAdapter( 215 | {}, 216 | { 217 | provider: "sqlite", 218 | schema: {}, 219 | }, 220 | )({} as BetterAuthOptions), 221 | options: { 222 | database: drizzleAdapter( 223 | {}, 224 | { 225 | provider: "sqlite", 226 | schema: {}, 227 | }, 228 | ), 229 | plugins: [passkey()], 230 | user: { 231 | modelName: "custom_user", 232 | }, 233 | account: { 234 | modelName: "custom_account", 235 | }, 236 | session: { 237 | modelName: "custom_session", 238 | }, 239 | verification: { 240 | modelName: "custom_verification", 241 | }, 242 | }, 243 | }); 244 | expect(schema.code).toMatchFileSnapshot( 245 | "./__snapshots__/auth-schema-sqlite-passkey.txt", 246 | ); 247 | }); 248 | 249 | it("should generate drizzle schema for PostgreSQL with passkey plugin", async () => { 250 | const schema = await generateDrizzleSchema({ 251 | file: "test.drizzle", 252 | adapter: drizzleAdapter( 253 | {}, 254 | { 255 | provider: "pg", 256 | schema: {}, 257 | }, 258 | )({} as BetterAuthOptions), 259 | options: { 260 | database: drizzleAdapter( 261 | {}, 262 | { 263 | provider: "pg", 264 | schema: {}, 265 | }, 266 | ), 267 | plugins: [passkey()], 268 | user: { 269 | modelName: "custom_user", 270 | }, 271 | account: { 272 | modelName: "custom_account", 273 | }, 274 | session: { 275 | modelName: "custom_session", 276 | }, 277 | verification: { 278 | modelName: "custom_verification", 279 | }, 280 | }, 281 | }); 282 | expect(schema.code).toMatchFileSnapshot( 283 | "./__snapshots__/auth-schema-pg-passkey.txt", 284 | ); 285 | }); 286 | 287 | it("should generate drizzle schema for MySQL with passkey plugin and number id", async () => { 288 | const schema = await generateDrizzleSchema({ 289 | file: "test.drizzle", 290 | adapter: drizzleAdapter( 291 | {}, 292 | { 293 | provider: "mysql", 294 | schema: {}, 295 | }, 296 | )({} as BetterAuthOptions), 297 | options: { 298 | database: drizzleAdapter( 299 | {}, 300 | { 301 | provider: "mysql", 302 | schema: {}, 303 | }, 304 | ), 305 | plugins: [passkey()], 306 | advanced: { 307 | database: { 308 | useNumberId: true, 309 | }, 310 | }, 311 | user: { 312 | modelName: "custom_user", 313 | }, 314 | account: { 315 | modelName: "custom_account", 316 | }, 317 | session: { 318 | modelName: "custom_session", 319 | }, 320 | verification: { 321 | modelName: "custom_verification", 322 | }, 323 | }, 324 | }); 325 | expect(schema.code).toMatchFileSnapshot( 326 | "./__snapshots__/auth-schema-mysql-passkey-number-id.txt", 327 | ); 328 | }); 329 | 330 | it("should generate drizzle schema for SQLite with passkey plugin and number id", async () => { 331 | const schema = await generateDrizzleSchema({ 332 | file: "test.drizzle", 333 | adapter: drizzleAdapter( 334 | {}, 335 | { 336 | provider: "sqlite", 337 | schema: {}, 338 | }, 339 | )({} as BetterAuthOptions), 340 | options: { 341 | database: drizzleAdapter( 342 | {}, 343 | { 344 | provider: "sqlite", 345 | schema: {}, 346 | }, 347 | ), 348 | plugins: [passkey()], 349 | advanced: { 350 | database: { 351 | useNumberId: true, 352 | }, 353 | }, 354 | user: { 355 | modelName: "custom_user", 356 | }, 357 | account: { 358 | modelName: "custom_account", 359 | }, 360 | session: { 361 | modelName: "custom_session", 362 | }, 363 | verification: { 364 | modelName: "custom_verification", 365 | }, 366 | }, 367 | }); 368 | expect(schema.code).toMatchFileSnapshot( 369 | "./__snapshots__/auth-schema-sqlite-passkey-number-id.txt", 370 | ); 371 | }); 372 | }); 373 | ``` -------------------------------------------------------------------------------- /docs/content/blogs/1-3.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Better Auth 1.3" 3 | description: "SSO with SAML, Multi Team Support, Additional Fields for Organization, Performance and more." 4 | date: 2025-07-19 5 | author: 6 | name: "Bereket Engida" 7 | avatar: "/avatars/beka.jpg" 8 | twitter: "iambereket" 9 | image: "/release-og/1-3.png" 10 | tags: ["1.3", "authentication", "oidc", "mcp", "sso", "organization"] 11 | --- 12 | 13 | ## Better Auth 1.3 Release 14 | 15 | We're excited to announce the release of Better Auth 1.3. This release includes a lot of new features and improvements. 16 | 17 | To upgrade, run: 18 | 19 | ```package-install 20 | npm install [email protected] 21 | ``` 22 | 23 | --- 24 | 25 | ## 🚀 Highlights 26 | 27 | ### **SSO Plugin** 28 | 29 | The SSO plugin has been moved to its own package and now supports both **OIDC** and **SAML 2.0**. 30 | 31 | 👉 [Read the SSO docs](/docs/plugins/sso) 32 | 33 | ```ts title="auth.ts" 34 | import { betterAuth } from "better-auth"; 35 | import { sso } from "@better-auth/sso"; 36 | 37 | export const auth = betterAuth({ 38 | plugins: [ 39 | sso({ 40 | oidc: { 41 | clientId: process.env.OIDC_CLIENT_ID!, 42 | clientSecret: process.env.OIDC_CLIENT_SECRET!, 43 | }, 44 | saml: { 45 | entryPoint: "https://example.com/saml", 46 | issuer: "better-auth-example", 47 | certificate: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", 48 | }, 49 | providersLimit: async (user) => { 50 | const plan = await getUserPlan(user); 51 | return plan.name === "pro" ? 10 : 1; 52 | }, 53 | }), 54 | ], 55 | }); 56 | ``` 57 | 58 | --- 59 | 60 | ### **OIDC & MCP Plugins – Now Stable** 61 | 62 | Both OIDC and MCP plugins are production‑ready. 63 | 64 | ✅ Features: 65 | 66 | * Refresh token support in discovery & token endpoints 67 | * JWKs and PKCE for public clients 68 | * Trusted clients 69 | * Encrypted & hashed client secrets 70 | 71 | 👉 [Read OIDC docs](/docs/plugins/oidc-provider) 72 | 👉 [Read MCP docs](/docs/plugins/mcp) 73 | 74 | ```ts title="auth.ts" 75 | import { mcp } from "better-auth/plugins"; 76 | 77 | export const auth = betterAuth({ 78 | plugins: [ 79 | mcp({ 80 | loginPage: "/login", 81 | }), 82 | ], 83 | }); 84 | ``` 85 | 86 | --- 87 | 88 | ### **Stripe Plugin is now production ready** 89 | 90 | The Stripe plugin is now stable and usage based pricing is coming very soon. 91 | 92 | 👉 [Read Stripe docs](/docs/plugins/stripe) 93 | 94 | 95 | ```ts title="auth.ts" 96 | import { betterAuth } from "better-auth"; 97 | import { stripe } from "@better-auth/stripe"; 98 | 99 | export const auth = betterAuth({ 100 | plugins: [ 101 | stripe({ 102 | // ... 103 | }), 104 | ], 105 | }); 106 | ``` 107 | 108 | ### **SIWE Plugin** 109 | 110 | Native support for **Sign‑In with Ethereum**. 111 | 112 | 👉 [Read SIWE docs](/docs/plugins/siwe) 113 | 114 | ```ts title="auth.ts" 115 | import { siwe } from "better-auth/plugins"; 116 | 117 | export const auth = betterAuth({ 118 | plugins: [ 119 | siwe(), 120 | ], 121 | }); 122 | ``` 123 | 124 | --- 125 | 126 | ### **New Social Providers** 127 | 128 | We’ve added providers for **Notion, Slack, Linear, and Faceit**. 129 | 130 | ```ts title="auth.ts" 131 | import { betterAuth } from "better-auth"; 132 | 133 | export const auth = betterAuth({ 134 | socialProviders: { 135 | notion: { /* ... */ }, 136 | slack: { /* ... */ }, 137 | linear: { /* ... */ }, 138 | faceit: { /* ... */ }, 139 | }, 140 | }); 141 | ``` 142 | 143 | --- 144 | 145 | ### **SvelteKit Cookie Helper Plugin** 146 | 147 | Utilities for handling cookies in SvelteKit server actions. 148 | 149 | <Callout type="warn"> 150 | Breaking change: `building` and `getRequestEvent` must now be passed in as props. 151 | </Callout> 152 | 153 | ```ts title="auth.ts" 154 | import { betterAuth } from "better-auth"; 155 | import { sveltekitCookies } from "better-auth/svelte-kit"; 156 | import { getRequestEvent } from "$app/server"; 157 | 158 | export const auth = betterAuth({ 159 | plugins: [sveltekitCookies(getRequestEvent)], 160 | }); 161 | ``` 162 | 163 | --- 164 | 165 | ### **Email Verification on Sign‑In** 166 | 167 | ```ts title="auth.ts" 168 | export const auth = betterAuth({ 169 | emailVerification: { 170 | sendOnSignIn: true, // sends a verification email on sign‑in if the user isn’t verified 171 | }, 172 | }); 173 | ``` 174 | 175 | --- 176 | 177 | ### **Multi‑Team Support** 178 | 179 | The organization plugin now supports members belonging to multiple teams. 180 | 181 | **Breaking change:** 182 | `teamId` has been removed from the `member` table. A new `teamMembers` table is required. 183 | 184 | ```ts title="auth.ts" 185 | export const auth = betterAuth({ 186 | plugins: [ 187 | organization({ 188 | // ... 189 | }), 190 | ], 191 | }); 192 | ``` 193 | 194 | ```ts title="auth-client.ts" 195 | import { createAuthClient } from "better-auth/client"; 196 | import { organizationClient } from "better-auth/client/plugins"; 197 | import { auth } from "./auth"; 198 | 199 | export const authClient = createAuthClient({ 200 | // pass your auth instance to infer additional fields 201 | plugins: [organizationClient({ $inferAuth: {} as typeof auth })], // [!code highlight] 202 | }); 203 | ``` 204 | 205 | --- 206 | 207 | ### **Additional Organization Fields** 208 | 209 | Add custom fields to `organization`, `member`, and `invitation` models. 210 | 211 | ```ts title="auth.ts" 212 | export const auth = betterAuth({ 213 | plugins: [ 214 | organization({ 215 | schema: { 216 | organization: { additionalFields: { /* ... */ } }, 217 | member: { additionalFields: { /* ... */ } }, 218 | invitation: { additionalFields: { /* ... */ } }, 219 | }, 220 | }), 221 | ], 222 | }); 223 | ``` 224 | 225 | Other new options: 226 | 227 | * `maximumMembersPerTeam` – set team member limits 228 | * `listUserInvitations` – list all invitations for a user 229 | 230 | --- 231 | 232 | ### **Generic OAuth Improvements** 233 | 234 | * Added support for extra token URL params 235 | * OAuth token encryption options 236 | 237 | ```ts title="auth.ts" 238 | export const auth = betterAuth({ 239 | plugins: [ 240 | genericOAuth({ 241 | // ... 242 | }), 243 | ], 244 | }); 245 | ``` 246 | 247 | --- 248 | 249 | ### **API Keys** 250 | 251 | * `requireName` option for key creation 252 | * `verifyKey` now supports async functions 253 | 254 | --- 255 | 256 | ### **Username** 257 | 258 | * Availability checks 259 | * Custom normalization 260 | 261 | --- 262 | 263 | ### ✨ More Features 264 | 265 | * Migrated to **Zod 4** for better type safety and performance 266 | * CLI supports custom adapter `createSchema` 267 | * `inferAuth` utility to infer types from the client 268 | * Improved docs with `auth` and `authClient` examples 269 | * `rememberMe` support in `signUp` 270 | * `afterEmailVerification` hook 271 | * `freshAge` and custom `errorURL` respected properly 272 | * OAuth2 tokens now include `refresh_token_expires_in` 273 | 274 | --- 275 | 276 | ### 🐛 Bug Fixes & Improvements 277 | 278 | #### Plugins 279 | 280 | * Expo: Fixed type path import 281 | * SSO: Fixed SAML redirection & type checks 282 | * Dropbox: Token access type support 283 | * Stripe: 284 | 285 | * Prevent duplicate customers 286 | * Allow upgrading incomplete subscriptions 287 | * Admin: 288 | 289 | * Fixed missing `ctx` in hooks 290 | * Proper error when removing invalid user IDs 291 | 292 | #### OAuth & Providers 293 | 294 | * Fixed duplicate OAuth registration 295 | * Improved Google/Microsoft scope handling 296 | * Fixed malformed error URLs in generic OAuth 297 | * Facebook: Better detection for limited token JWT 298 | * Twitter: Improved email verification logic 299 | 300 | #### Core Authentication 301 | 302 | * Exclude current user from username uniqueness check 303 | * Support `callbackURL` in `signInUsername` 304 | * Allow account linking without email 305 | * Fixed missing `null` type in `/get-session` response 306 | * Global `onSuccess` hook now works 307 | * JWT: Alternate algorithms supported in JWKS 308 | * `origin-check`: Wildcard trusted origins supported 309 | 310 | #### CLI, DB, and Adapters 311 | 312 | * CLI: Improved Drizzle schema formatting 313 | * MongoAdapter: Works with `create-adapter` 314 | * Schema generation respects `useNumberId` 315 | * Postgres: Better varchar normalization and type comparison 316 | * Drizzle CLI: Uses `serial` as PK if `useNumberId` is enabled 317 | 318 | #### Email & OTP 319 | 320 | * OTPs now encrypted 321 | * Fixed `onEmailVerification` not firing 322 | * Proper error when sign‑up is disabled 323 | * Phone number: Reset clears verification values 324 | 325 | #### Two-Factor Auth 326 | 327 | * Default OTP period fix 328 | * URI generation doesn’t require enabling 2FA 329 | * Fixed OTP URI separator mismatch 330 | 331 | #### Miscellaneous 332 | 333 | * Delete organization if member not found 334 | * Correct error codes for API key rate limits 335 | * Additional fields now show in OpenAPI 336 | * Fixed FK constraint generation for MySQL 337 | * Various improvements to account linking 338 | * OIDC `offline_access` no longer requires `prompt=consent` 339 | * Fixed malformed base64 encoding for token validation 340 | 341 | --- 342 | 343 | A lot of refinements to make everything smoother, faster, and more reliable. 344 | 👉 [Check the full changelog](https://github.com/better-auth/better-auth/releases/tag/v1.3.0) 345 | 346 | --- 347 | ``` -------------------------------------------------------------------------------- /docs/components/builder/code-tabs/code-tabs.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { cn } from "@/lib/utils"; 2 | interface TabProps { 3 | fileName: string; 4 | isActive: boolean; 5 | brightnessLevel?: number; // New optional prop for brightness level 6 | onClick: () => void; 7 | onClose: () => void; 8 | } 9 | 10 | const brightnessLevels = [ 11 | "bg-background", 12 | "bg-background-200", // 13 | "bg-background-300", 14 | "bg-background-400", 15 | "bg-background-500", 16 | "bg-background-600", 17 | "bg-background-700", 18 | ]; 19 | 20 | export function CodeTab({ 21 | fileName, 22 | isActive, 23 | brightnessLevel = 0, 24 | onClick, 25 | onClose, 26 | }: TabProps) { 27 | const activeBrightnessClass = isActive 28 | ? brightnessLevels[brightnessLevel % brightnessLevels.length] 29 | : "bg-muted"; 30 | 31 | const textColor = isActive ? "text-foreground" : "text-muted-foreground"; 32 | const borderColor = isActive 33 | ? "border-t-foreground" 34 | : "border-t-transparent hover:bg-background/50"; 35 | 36 | return ( 37 | <div 38 | className={cn( 39 | "flex items-center px-3 py-2 gap-2 text-sm font-medium border-t-2 cursor-pointer transition-colors duration-200", 40 | activeBrightnessClass, 41 | textColor, 42 | borderColor, 43 | )} 44 | onClick={onClick} 45 | > 46 | {fileName.endsWith(".ts") && ( 47 | <svg 48 | xmlns="http://www.w3.org/2000/svg" 49 | width="1em" 50 | height="1em" 51 | viewBox="0 0 128 128" 52 | > 53 | <path 54 | className="fill-current" 55 | d="M2 63.91v62.5h125v-125H2zm100.73-5a15.56 15.56 0 0 1 7.82 4.5a20.6 20.6 0 0 1 3 4c0 .16-5.4 3.81-8.69 5.85c-.12.08-.6-.44-1.13-1.23a7.09 7.09 0 0 0-5.87-3.53c-3.79-.26-6.23 1.73-6.21 5a4.6 4.6 0 0 0 .54 2.34c.83 1.73 2.38 2.76 7.24 4.86c8.95 3.85 12.78 6.39 15.16 10c2.66 4 3.25 10.46 1.45 15.24c-2 5.2-6.9 8.73-13.83 9.9a38.3 38.3 0 0 1-9.52-.1A23 23 0 0 1 80 109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9 9 0 0 1 1.15-.73l4.6-2.64l3.59-2.08l.75 1.11a16.8 16.8 0 0 0 4.74 4.54c4 2.1 9.46 1.81 12.16-.62a5.43 5.43 0 0 0 .69-6.92c-1-1.39-3-2.56-8.59-5c-6.45-2.78-9.23-4.5-11.77-7.24a16.5 16.5 0 0 1-3.43-6.25a25 25 0 0 1-.22-8c1.33-6.23 6-10.58 12.82-11.87a31.7 31.7 0 0 1 9.49.26zm-29.34 5.24v5.12H57.16v46.23H45.65V69.26H29.38v-5a49 49 0 0 1 .14-5.16c.06-.08 10-.12 22-.1h21.81z" 56 | ></path> 57 | </svg> 58 | )} 59 | {fileName.endsWith(".tsx") && ( 60 | <svg 61 | fill="currentColor" 62 | xmlns="http://www.w3.org/2000/svg" 63 | width="1.3em" 64 | height="1.3em" 65 | viewBox="0 0 30 30" 66 | > 67 | <path 68 | className="fill-current" 69 | d="M 10.679688 4.1816406 C 10.068687 4.1816406 9.502 4.3184219 9 4.6074219 C 7.4311297 5.5132122 6.8339651 7.7205462 7.1503906 10.46875 C 4.6127006 11.568833 3 13.188667 3 15 C 3 16.811333 4.6127006 18.431167 7.1503906 19.53125 C 6.8341285 22.279346 7.4311297 24.486788 9 25.392578 C 9.501 25.681578 10.067687 25.818359 10.679688 25.818359 C 11.982314 25.818359 13.48785 25.164589 15 24.042969 C 16.512282 25.164589 18.01964 25.818359 19.322266 25.818359 C 19.933266 25.818359 20.499953 25.681578 21.001953 25.392578 C 22.570814 24.486793 23.167976 22.279432 22.851562 19.53125 C 25.388297 18.431178 27 16.81094 27 15 C 27 13.188667 25.387299 11.568833 22.849609 10.46875 C 23.165872 7.7206538 22.56887 5.5132122 21 4.6074219 C 20.499 4.3174219 19.932312 4.1816406 19.320312 4.1816406 C 18.017686 4.1816406 16.51215 4.8354109 15 5.9570312 C 13.487763 4.8354109 11.981863 4.1816406 10.679688 4.1816406 z M 10.679688 5.9316406 C 11.461321 5.9316406 12.49496 6.3472486 13.617188 7.1171875 C 12.95737 7.7398717 12.311153 8.4479321 11.689453 9.2363281 C 10.681079 9.3809166 9.7303472 9.5916908 8.8496094 9.8554688 C 8.8448793 9.7943902 8.8336776 9.7303008 8.8300781 9.6699219 C 8.7230781 7.8899219 9.114 6.5630469 9.875 6.1230469 C 10.1 5.9930469 10.362688 5.9316406 10.679688 5.9316406 z M 19.320312 5.9316406 C 19.636312 5.9316406 19.9 5.9930469 20.125 6.1230469 C 20.886 6.5620469 21.276922 7.8899219 21.169922 9.6699219 C 21.166295 9.7303008 21.155145 9.7943902 21.150391 9.8554688 C 20.2691 9.5915252 19.317669 9.3809265 18.308594 9.2363281 C 17.686902 8.4480417 17.042616 7.7397993 16.382812 7.1171875 C 17.504962 6.3473772 18.539083 5.9316406 19.320312 5.9316406 z M 15 8.2285156 C 15.27108 8.4752506 15.540266 8.7360345 15.8125 9.0214844 C 15.542718 9.012422 15.274373 9 15 9 C 14.726286 9 14.458598 9.0124652 14.189453 9.0214844 C 14.461446 8.7363308 14.729174 8.4750167 15 8.2285156 z M 15 10.75 C 15.828688 10.75 16.614128 10.796321 17.359375 10.876953 C 17.813861 11.494697 18.261774 12.147811 18.681641 12.875 C 19.084074 13.572033 19.439938 14.285488 19.753906 15 C 19.439896 15.714942 19.084316 16.429502 18.681641 17.126953 C 18.263078 17.852044 17.816279 18.500949 17.363281 19.117188 C 16.591711 19.201607 15.800219 19.25 15 19.25 C 14.171312 19.25 13.385872 19.203679 12.640625 19.123047 C 12.186139 18.505303 11.738226 17.854142 11.318359 17.126953 C 10.915684 16.429502 10.560194 15.714942 10.246094 15 C 10.559972 14.285488 10.915926 13.572033 11.318359 12.875 C 11.737083 12.149909 12.183612 11.499051 12.636719 10.882812 C 13.408289 10.798393 14.199781 10.75 15 10.75 z M 19.746094 11.291016 C 20.142841 11.386804 20.524253 11.490209 20.882812 11.605469 C 20.801579 11.97252 20.702235 12.346608 20.589844 12.724609 C 20.461164 12.483141 20.336375 12.240903 20.197266 12 C 20.054139 11.752196 19.895244 11.529558 19.746094 11.291016 z M 10.251953 11.292969 C 10.103305 11.530776 9.9454023 11.752991 9.8027344 12 C 9.6636666 12.240944 9.5387971 12.483106 9.4101562 12.724609 C 9.29751 12.345829 9.1965499 11.971295 9.1152344 11.603516 C 9.4803698 11.48815 9.86083 11.385986 10.251953 11.292969 z M 7.46875 12.246094 C 7.6794464 13.135714 7.9717297 14.057918 8.3476562 14.998047 C 7.9725263 15.935943 7.6814729 16.856453 7.4707031 17.744141 C 5.7292327 16.903203 4.75 15.856373 4.75 15 C 4.75 14.121 5.701875 13.119266 7.296875 12.322266 C 7.3513169 12.295031 7.4131225 12.272692 7.46875 12.246094 z M 22.529297 12.255859 C 24.270767 13.096797 25.25 14.143627 25.25 15 C 25.25 15.879 24.298125 16.880734 22.703125 17.677734 C 22.648683 17.704969 22.586877 17.727308 22.53125 17.753906 C 22.32043 16.863764 22.030541 15.940699 21.654297 15 C 22.028977 14.062913 22.318703 13.142804 22.529297 12.255859 z M 15 13 C 13.895 13 13 13.895 13 15 C 13 16.105 13.895 17 15 17 C 16.105 17 17 16.105 17 15 C 17 13.895 16.105 13 15 13 z M 9.4101562 17.275391 C 9.5388794 17.516948 9.6655262 17.759008 9.8046875 18 C 9.9476585 18.247625 10.104915 18.470608 10.253906 18.708984 C 9.857159 18.613196 9.4757466 18.509791 9.1171875 18.394531 C 9.1984813 18.02725 9.2976676 17.653633 9.4101562 17.275391 z M 20.589844 17.277344 C 20.702364 17.655759 20.803517 18.02905 20.884766 18.396484 C 20.51963 18.51185 20.13917 18.614014 19.748047 18.707031 C 19.896695 18.469224 20.054598 18.247009 20.197266 18 C 20.336044 17.759557 20.461449 17.518344 20.589844 17.277344 z M 8.8496094 20.144531 C 9.7309004 20.408475 10.682331 20.619073 11.691406 20.763672 C 12.313288 21.552345 12.957085 22.261935 13.617188 22.884766 C 12.495042 23.654481 11.461272 24.070312 10.679688 24.070312 C 10.363687 24.070312 10.1 24.006953 9.875 23.876953 C 9.114 23.437953 8.7230781 22.112031 8.8300781 20.332031 C 8.8337424 20.271023 8.8447938 20.206253 8.8496094 20.144531 z M 21.150391 20.144531 C 21.155182 20.206253 21.166285 20.271023 21.169922 20.332031 C 21.276922 22.112031 20.886 23.436953 20.125 23.876953 C 19.9 24.006953 19.637312 24.070313 19.320312 24.070312 C 18.538728 24.070312 17.504958 23.654609 16.382812 22.884766 C 17.042964 22.261863 17.688542 21.552454 18.310547 20.763672 C 19.318921 20.619083 20.269653 20.408309 21.150391 20.144531 z M 14.1875 20.978516 C 14.457282 20.987578 14.725627 21 15 21 C 15.274373 21 15.542718 20.987578 15.8125 20.978516 C 15.540266 21.263964 15.27108 21.524765 15 21.771484 C 14.72892 21.524749 14.459734 21.263966 14.1875 20.978516 z" 70 | ></path> 71 | </svg> 72 | )} 73 | <span className="truncate max-w-[100px]">{fileName}</span> 74 | </div> 75 | ); 76 | } 77 | ``` -------------------------------------------------------------------------------- /docs/components/docs/layout/toc.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import type { TOCItemType } from "fumadocs-core/server"; 3 | import * as Primitive from "fumadocs-core/toc"; 4 | import { 5 | type ComponentProps, 6 | createContext, 7 | type HTMLAttributes, 8 | type ReactNode, 9 | use, 10 | useEffect, 11 | useMemo, 12 | useRef, 13 | useState, 14 | } from "react"; 15 | import { cn } from "@/lib/utils"; 16 | import { useI18n } from "fumadocs-ui/provider"; 17 | import { TocThumb } from "./toc-thumb"; 18 | import { ScrollArea, ScrollViewport } from "../ui/scroll-area"; 19 | import type { 20 | PopoverContentProps, 21 | PopoverTriggerProps, 22 | } from "@radix-ui/react-popover"; 23 | import { ChevronRight, Text } from "lucide-react"; 24 | import { usePageStyles } from "fumadocs-ui/provider"; 25 | import { 26 | Collapsible, 27 | CollapsibleContent, 28 | CollapsibleTrigger, 29 | } from "../ui/collapsible"; 30 | 31 | export interface TOCProps { 32 | /** 33 | * Custom content in TOC container, before the main TOC 34 | */ 35 | header?: ReactNode; 36 | 37 | /** 38 | * Custom content in TOC container, after the main TOC 39 | */ 40 | footer?: ReactNode; 41 | 42 | children: ReactNode; 43 | } 44 | 45 | export function Toc(props: HTMLAttributes<HTMLDivElement>) { 46 | const { toc } = usePageStyles(); 47 | 48 | return ( 49 | <div 50 | id="nd-toc" 51 | {...props} 52 | className={cn( 53 | "sticky top-[calc(var(--fd-banner-height)+var(--fd-nav-height))] h-(--fd-toc-height) pb-2 pt-12", 54 | toc, 55 | props.className, 56 | )} 57 | style={ 58 | { 59 | ...props.style, 60 | "--fd-toc-height": 61 | "calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))", 62 | } as object 63 | } 64 | > 65 | <div className="flex h-full w-(--fd-toc-width) max-w-full flex-col gap-3 pe-4"> 66 | {props.children} 67 | </div> 68 | </div> 69 | ); 70 | } 71 | 72 | export function TocItemsEmpty() { 73 | const { text } = useI18n(); 74 | 75 | return ( 76 | <div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground"> 77 | {text.tocNoHeadings} 78 | </div> 79 | ); 80 | } 81 | 82 | export function TOCScrollArea({ 83 | isMenu, 84 | ...props 85 | }: ComponentProps<typeof ScrollArea> & { isMenu?: boolean }) { 86 | const viewRef = useRef<HTMLDivElement>(null); 87 | 88 | return ( 89 | <ScrollArea 90 | {...props} 91 | className={cn("flex flex-col ps-px", props.className)} 92 | > 93 | <Primitive.ScrollProvider containerRef={viewRef}> 94 | <ScrollViewport 95 | className={cn( 96 | "relative min-h-0 text-sm", 97 | isMenu && "mt-2 mb-4 mx-4 md:mx-6", 98 | )} 99 | ref={viewRef} 100 | > 101 | {props.children} 102 | </ScrollViewport> 103 | </Primitive.ScrollProvider> 104 | </ScrollArea> 105 | ); 106 | } 107 | 108 | export function TOCItems({ items }: { items: TOCItemType[] }) { 109 | const containerRef = useRef<HTMLDivElement>(null); 110 | 111 | const [svg, setSvg] = useState<{ 112 | path: string; 113 | width: number; 114 | height: number; 115 | }>(); 116 | 117 | useEffect(() => { 118 | if (!containerRef.current) return; 119 | const container = containerRef.current; 120 | 121 | function onResize(): void { 122 | if (container.clientHeight === 0) return; 123 | let w = 0, 124 | h = 0; 125 | const d: string[] = []; 126 | for (let i = 0; i < items.length; i++) { 127 | const element: HTMLElement | null = container.querySelector( 128 | `a[href="#${items[i].url.slice(1)}"]`, 129 | ); 130 | if (!element) continue; 131 | 132 | const styles = getComputedStyle(element); 133 | const offset = getLineOffset(items[i].depth) + 1, 134 | top = element.offsetTop + parseFloat(styles.paddingTop), 135 | bottom = 136 | element.offsetTop + 137 | element.clientHeight - 138 | parseFloat(styles.paddingBottom); 139 | 140 | w = Math.max(offset, w); 141 | h = Math.max(h, bottom); 142 | 143 | d.push(`${i === 0 ? "M" : "L"}${offset} ${top}`); 144 | d.push(`L${offset} ${bottom}`); 145 | } 146 | 147 | setSvg({ 148 | path: d.join(" "), 149 | width: w + 1, 150 | height: h, 151 | }); 152 | } 153 | 154 | const observer = new ResizeObserver(onResize); 155 | onResize(); 156 | 157 | observer.observe(container); 158 | return () => { 159 | observer.disconnect(); 160 | }; 161 | }, [items]); 162 | 163 | if (items.length === 0) return <TocItemsEmpty />; 164 | 165 | return ( 166 | <> 167 | {svg ? ( 168 | <div 169 | className="absolute start-0 top-0 rtl:-scale-x-100" 170 | style={{ 171 | width: svg.width, 172 | height: svg.height, 173 | maskImage: `url("data:image/svg+xml,${ 174 | // Inline SVG 175 | encodeURIComponent( 176 | `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="1" fill="none" /></svg>`, 177 | ) 178 | }")`, 179 | }} 180 | > 181 | <TocThumb 182 | containerRef={containerRef} 183 | className="mt-(--fd-top) h-(--fd-height) bg-fd-primary transition-all" 184 | /> 185 | </div> 186 | ) : null} 187 | <div className="flex flex-col" ref={containerRef}> 188 | {items.map((item, i) => ( 189 | <TOCItem 190 | key={item.url} 191 | item={item} 192 | upper={items[i - 1]?.depth} 193 | lower={items[i + 1]?.depth} 194 | /> 195 | ))} 196 | </div> 197 | </> 198 | ); 199 | } 200 | 201 | function getItemOffset(depth: number): number { 202 | if (depth <= 2) return 14; 203 | if (depth === 3) return 26; 204 | return 36; 205 | } 206 | 207 | function getLineOffset(depth: number): number { 208 | return depth >= 3 ? 10 : 0; 209 | } 210 | 211 | function TOCItem({ 212 | item, 213 | upper = item.depth, 214 | lower = item.depth, 215 | }: { 216 | item: TOCItemType; 217 | upper?: number; 218 | lower?: number; 219 | }) { 220 | const offset = getLineOffset(item.depth), 221 | upperOffset = getLineOffset(upper), 222 | lowerOffset = getLineOffset(lower); 223 | 224 | return ( 225 | <Primitive.TOCItem 226 | href={item.url} 227 | style={{ 228 | paddingInlineStart: getItemOffset(item.depth), 229 | }} 230 | className="prose relative py-1.5 text-sm text-fd-muted-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-fd-primary" 231 | > 232 | {offset !== upperOffset ? ( 233 | <svg 234 | xmlns="http://www.w3.org/2000/svg" 235 | viewBox="0 0 16 16" 236 | className="absolute -top-1.5 start-0 size-4 rtl:-scale-x-100" 237 | > 238 | <line 239 | x1={upperOffset} 240 | y1="0" 241 | x2={offset} 242 | y2="12" 243 | className="stroke-fd-foreground/10" 244 | strokeWidth="1" 245 | /> 246 | </svg> 247 | ) : null} 248 | <div 249 | className={cn( 250 | "absolute inset-y-0 w-px bg-fd-foreground/10", 251 | offset !== upperOffset && "top-1.5", 252 | offset !== lowerOffset && "bottom-1.5", 253 | )} 254 | style={{ 255 | insetInlineStart: offset, 256 | }} 257 | /> 258 | {item.title} 259 | </Primitive.TOCItem> 260 | ); 261 | } 262 | 263 | type MakeRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }; 264 | 265 | const Context = createContext<{ 266 | open: boolean; 267 | setOpen: (open: boolean) => void; 268 | } | null>(null); 269 | 270 | const TocProvider = Context.Provider || Context; 271 | 272 | export function TocPopover({ 273 | open, 274 | onOpenChange, 275 | ref: _ref, 276 | ...props 277 | }: MakeRequired<ComponentProps<typeof Collapsible>, "open" | "onOpenChange">) { 278 | return ( 279 | <Collapsible open={open} onOpenChange={onOpenChange} {...props}> 280 | <TocProvider 281 | value={useMemo( 282 | () => ({ 283 | open, 284 | setOpen: onOpenChange, 285 | }), 286 | [onOpenChange, open], 287 | )} 288 | > 289 | {props.children} 290 | </TocProvider> 291 | </Collapsible> 292 | ); 293 | } 294 | 295 | export function TocPopoverTrigger({ 296 | items, 297 | ...props 298 | }: PopoverTriggerProps & { items: TOCItemType[] }) { 299 | const { text } = useI18n(); 300 | const { open } = use(Context)!; 301 | const active = Primitive.useActiveAnchor(); 302 | const current = useMemo(() => { 303 | return items.find((item) => active === item.url.slice(1))?.title; 304 | }, [items, active]); 305 | 306 | return ( 307 | <CollapsibleTrigger 308 | {...props} 309 | className={cn( 310 | "inline-flex items-center text-sm gap-2 text-nowrap px-4 py-2.5 text-start md:px-6 focus-visible:outline-none", 311 | props.className, 312 | )} 313 | > 314 | <Text className="size-4 shrink-0" /> 315 | {text.toc} 316 | <ChevronRight 317 | className={cn( 318 | "size-4 shrink-0 text-fd-muted-foreground transition-all", 319 | !current && "opacity-0", 320 | open ? "rotate-90" : "-ms-1.5", 321 | )} 322 | /> 323 | <span 324 | className={cn( 325 | "truncate text-fd-muted-foreground transition-opacity -ms-1.5", 326 | (!current || open) && "opacity-0", 327 | )} 328 | > 329 | {current} 330 | </span> 331 | </CollapsibleTrigger> 332 | ); 333 | } 334 | 335 | export function TocPopoverContent(props: PopoverContentProps) { 336 | return ( 337 | <CollapsibleContent 338 | data-toc-popover="" 339 | className="flex flex-col max-h-[50vh]" 340 | {...props} 341 | > 342 | {props.children} 343 | </CollapsibleContent> 344 | ); 345 | } 346 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/test-utils/test-instance.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AsyncLocalStorage } from "node:async_hooks"; 2 | import { afterAll } from "vitest"; 3 | import { betterAuth } from "../auth"; 4 | import { createAuthClient } from "../client/vanilla"; 5 | import type { Session, User } from "../types"; 6 | import type { BetterAuthClientOptions } from "@better-auth/core"; 7 | import { getMigrations } from "../db/get-migration"; 8 | import { parseSetCookieHeader, setCookieToHeader } from "../cookies"; 9 | import type { SuccessContext } from "@better-fetch/fetch"; 10 | import { getAdapter } from "../db/utils"; 11 | import Database from "better-sqlite3"; 12 | import { getBaseURL } from "../utils/url"; 13 | import { Kysely, MysqlDialect, PostgresDialect, sql } from "kysely"; 14 | import { Pool } from "pg"; 15 | import { MongoClient } from "mongodb"; 16 | import { mongodbAdapter } from "../adapters/mongodb-adapter"; 17 | import { createPool } from "mysql2/promise"; 18 | import { bearer } from "../plugins"; 19 | import type { BetterAuthOptions } from "@better-auth/core"; 20 | 21 | const cleanupSet = new Set<Function>(); 22 | 23 | type CurrentUserContext = { 24 | headers: Headers; 25 | }; 26 | const currentUserContextStorage = new AsyncLocalStorage<CurrentUserContext>(); 27 | 28 | afterAll(async () => { 29 | for (const cleanup of cleanupSet) { 30 | await cleanup(); 31 | cleanupSet.delete(cleanup); 32 | } 33 | }); 34 | 35 | export async function getTestInstance< 36 | O extends Partial<BetterAuthOptions>, 37 | C extends BetterAuthClientOptions, 38 | >( 39 | options?: O, 40 | config?: { 41 | clientOptions?: C; 42 | port?: number; 43 | disableTestUser?: boolean; 44 | testUser?: Partial<User>; 45 | testWith?: "sqlite" | "postgres" | "mongodb" | "mysql"; 46 | }, 47 | ) { 48 | const testWith = config?.testWith || "sqlite"; 49 | 50 | const postgres = new Kysely({ 51 | dialect: new PostgresDialect({ 52 | pool: new Pool({ 53 | connectionString: "postgres://user:password@localhost:5432/better_auth", 54 | }), 55 | }), 56 | }); 57 | 58 | const sqlite = new Database(":memory:"); 59 | 60 | const mysql = new Kysely({ 61 | dialect: new MysqlDialect( 62 | createPool("mysql://user:password@localhost:3306/better_auth"), 63 | ), 64 | }); 65 | 66 | async function mongodbClient() { 67 | const dbClient = async (connectionString: string, dbName: string) => { 68 | const client = new MongoClient(connectionString); 69 | await client.connect(); 70 | const db = client.db(dbName); 71 | return db; 72 | }; 73 | const db = await dbClient("mongodb://127.0.0.1:27017", "better-auth"); 74 | return db; 75 | } 76 | 77 | const opts = { 78 | socialProviders: { 79 | github: { 80 | clientId: "test", 81 | clientSecret: "test", 82 | }, 83 | google: { 84 | clientId: "test", 85 | clientSecret: "test", 86 | }, 87 | }, 88 | secret: "better-auth.secret", 89 | database: 90 | testWith === "postgres" 91 | ? { db: postgres, type: "postgres" } 92 | : testWith === "mongodb" 93 | ? mongodbAdapter(await mongodbClient()) 94 | : testWith === "mysql" 95 | ? { db: mysql, type: "mysql" } 96 | : sqlite, 97 | emailAndPassword: { 98 | enabled: true, 99 | }, 100 | rateLimit: { 101 | enabled: false, 102 | }, 103 | advanced: { 104 | cookies: {}, 105 | }, 106 | logger: { 107 | level: "debug", 108 | }, 109 | } satisfies BetterAuthOptions; 110 | 111 | const auth = betterAuth({ 112 | baseURL: "http://localhost:" + (config?.port || 3000), 113 | ...opts, 114 | ...options, 115 | plugins: [bearer(), ...(options?.plugins || [])], 116 | } as unknown as O); 117 | 118 | const testUser = { 119 | email: "[email protected]", 120 | password: "test123456", 121 | name: "test user", 122 | ...config?.testUser, 123 | }; 124 | async function createTestUser() { 125 | if (config?.disableTestUser) { 126 | return; 127 | } 128 | //@ts-expect-error 129 | await auth.api.signUpEmail({ 130 | body: testUser, 131 | }); 132 | } 133 | 134 | if (testWith !== "mongodb") { 135 | const { runMigrations } = await getMigrations({ 136 | ...auth.options, 137 | database: opts.database, 138 | }); 139 | await runMigrations(); 140 | } 141 | 142 | await createTestUser(); 143 | 144 | const cleanup = async () => { 145 | if (testWith === "mongodb") { 146 | const db = await mongodbClient(); 147 | await db.dropDatabase(); 148 | return; 149 | } 150 | if (testWith === "postgres") { 151 | await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute( 152 | postgres, 153 | ); 154 | await postgres.destroy(); 155 | return; 156 | } 157 | 158 | if (testWith === "mysql") { 159 | await sql`SET FOREIGN_KEY_CHECKS = 0;`.execute(mysql); 160 | const tables = await mysql.introspection.getTables(); 161 | for (const table of tables) { 162 | // @ts-expect-error 163 | await mysql.deleteFrom(table.name).execute(); 164 | } 165 | await sql`SET FOREIGN_KEY_CHECKS = 1;`.execute(mysql); 166 | return; 167 | } 168 | if (testWith === "sqlite") { 169 | sqlite.close(); 170 | return; 171 | } 172 | }; 173 | cleanupSet.add(cleanup); 174 | 175 | const customFetchImpl = async ( 176 | url: string | URL | Request, 177 | init?: RequestInit, 178 | ) => { 179 | const headers = init?.headers || {}; 180 | const storageHeaders = currentUserContextStorage.getStore()?.headers; 181 | return auth.handler( 182 | new Request( 183 | url, 184 | init 185 | ? { 186 | ...init, 187 | headers: new Headers({ 188 | ...(storageHeaders 189 | ? Object.fromEntries(storageHeaders.entries()) 190 | : {}), 191 | ...(headers instanceof Headers 192 | ? Object.fromEntries(headers.entries()) 193 | : typeof headers === "object" 194 | ? headers 195 | : {}), 196 | }), 197 | } 198 | : { 199 | headers, 200 | }, 201 | ), 202 | ); 203 | }; 204 | 205 | const client = createAuthClient({ 206 | ...(config?.clientOptions as C extends undefined ? {} : C), 207 | baseURL: getBaseURL( 208 | options?.baseURL || "http://localhost:" + (config?.port || 3000), 209 | options?.basePath || "/api/auth", 210 | ), 211 | fetchOptions: { 212 | customFetchImpl, 213 | }, 214 | }); 215 | 216 | async function signInWithTestUser() { 217 | if (config?.disableTestUser) { 218 | throw new Error("Test user is disabled"); 219 | } 220 | let headers = new Headers(); 221 | const setCookie = (name: string, value: string) => { 222 | const current = headers.get("cookie"); 223 | headers.set("cookie", `${current || ""}; ${name}=${value}`); 224 | }; 225 | //@ts-expect-error 226 | const { data, error } = await client.signIn.email({ 227 | email: testUser.email, 228 | password: testUser.password, 229 | fetchOptions: { 230 | //@ts-expect-error 231 | onSuccess(context) { 232 | const header = context.response.headers.get("set-cookie"); 233 | const cookies = parseSetCookieHeader(header || ""); 234 | const signedCookie = cookies.get("better-auth.session_token")?.value; 235 | headers.set("cookie", `better-auth.session_token=${signedCookie}`); 236 | }, 237 | }, 238 | }); 239 | return { 240 | session: data.session as Session, 241 | user: data.user as User, 242 | headers, 243 | setCookie, 244 | runWithUser: async (fn: (headers: Headers) => Promise<void>) => { 245 | return currentUserContextStorage.run({ headers }, async () => { 246 | await fn(headers); 247 | }); 248 | }, 249 | }; 250 | } 251 | async function signInWithUser(email: string, password: string) { 252 | const headers = new Headers(); 253 | //@ts-expect-error 254 | const { data } = await client.signIn.email({ 255 | email, 256 | password, 257 | fetchOptions: { 258 | //@ts-expect-error 259 | onSuccess(context) { 260 | const header = context.response.headers.get("set-cookie"); 261 | const cookies = parseSetCookieHeader(header || ""); 262 | const signedCookie = cookies.get("better-auth.session_token")?.value; 263 | headers.set("cookie", `better-auth.session_token=${signedCookie}`); 264 | }, 265 | }, 266 | }); 267 | return { 268 | res: data as { 269 | user: User; 270 | session: Session; 271 | }, 272 | headers, 273 | }; 274 | } 275 | 276 | function sessionSetter(headers: Headers) { 277 | return (context: SuccessContext) => { 278 | const header = context.response.headers.get("set-cookie"); 279 | if (header) { 280 | const cookies = parseSetCookieHeader(header || ""); 281 | const signedCookie = cookies.get("better-auth.session_token")?.value; 282 | headers.set("cookie", `better-auth.session_token=${signedCookie}`); 283 | } 284 | }; 285 | } 286 | 287 | return { 288 | auth, 289 | client, 290 | testUser, 291 | signInWithTestUser, 292 | signInWithUser, 293 | cookieSetter: setCookieToHeader, 294 | customFetchImpl, 295 | sessionSetter, 296 | db: await getAdapter(auth.options), 297 | runWithUser: async ( 298 | email: string, 299 | password: string, 300 | fn: (headers: Headers) => Promise<void> | void, 301 | ) => { 302 | const { headers } = await signInWithUser(email, password); 303 | return currentUserContextStorage.run({ headers }, async () => { 304 | await fn(headers); 305 | }); 306 | }, 307 | }; 308 | } 309 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function DropdownMenu({ 10 | ...props 11 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { 12 | return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; 13 | } 14 | 15 | function DropdownMenuPortal({ 16 | ...props 17 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { 18 | return ( 19 | <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> 20 | ); 21 | } 22 | 23 | function DropdownMenuTrigger({ 24 | ...props 25 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { 26 | return ( 27 | <DropdownMenuPrimitive.Trigger 28 | data-slot="dropdown-menu-trigger" 29 | {...props} 30 | /> 31 | ); 32 | } 33 | 34 | function DropdownMenuContent({ 35 | className, 36 | sideOffset = 4, 37 | ...props 38 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { 39 | return ( 40 | <DropdownMenuPrimitive.Portal> 41 | <DropdownMenuPrimitive.Content 42 | data-slot="dropdown-menu-content" 43 | sideOffset={sideOffset} 44 | className={cn( 45 | "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", 46 | className, 47 | )} 48 | {...props} 49 | /> 50 | </DropdownMenuPrimitive.Portal> 51 | ); 52 | } 53 | 54 | function DropdownMenuGroup({ 55 | ...props 56 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { 57 | return ( 58 | <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> 59 | ); 60 | } 61 | 62 | function DropdownMenuItem({ 63 | className, 64 | inset, 65 | variant = "default", 66 | ...props 67 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { 68 | inset?: boolean; 69 | variant?: "default" | "destructive"; 70 | }) { 71 | return ( 72 | <DropdownMenuPrimitive.Item 73 | data-slot="dropdown-menu-item" 74 | data-inset={inset} 75 | data-variant={variant} 76 | className={cn( 77 | "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:text-destructive-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:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 78 | className, 79 | )} 80 | {...props} 81 | /> 82 | ); 83 | } 84 | 85 | function DropdownMenuCheckboxItem({ 86 | className, 87 | children, 88 | checked, 89 | ...props 90 | }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { 91 | return ( 92 | <DropdownMenuPrimitive.CheckboxItem 93 | data-slot="dropdown-menu-checkbox-item" 94 | className={cn( 95 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 96 | className, 97 | )} 98 | checked={checked} 99 | {...props} 100 | > 101 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 102 | <DropdownMenuPrimitive.ItemIndicator> 103 | <CheckIcon className="size-4" /> 104 | </DropdownMenuPrimitive.ItemIndicator> 105 | </span> 106 | {children} 107 | </DropdownMenuPrimitive.CheckboxItem> 108 | ); 109 | } 110 | 111 | function DropdownMenuRadioGroup({ 112 | ...props 113 | }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { 114 | return ( 115 | <DropdownMenuPrimitive.RadioGroup 116 | data-slot="dropdown-menu-radio-group" 117 | {...props} 118 | /> 119 | ); 120 | } 121 | 122 | function DropdownMenuRadioItem({ 123 | className, 124 | children, 125 | ...props 126 | }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { 127 | return ( 128 | <DropdownMenuPrimitive.RadioItem 129 | data-slot="dropdown-menu-radio-item" 130 | className={cn( 131 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 132 | className, 133 | )} 134 | {...props} 135 | > 136 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 137 | <DropdownMenuPrimitive.ItemIndicator> 138 | <CircleIcon className="size-2 fill-current" /> 139 | </DropdownMenuPrimitive.ItemIndicator> 140 | </span> 141 | {children} 142 | </DropdownMenuPrimitive.RadioItem> 143 | ); 144 | } 145 | 146 | function DropdownMenuLabel({ 147 | className, 148 | inset, 149 | ...props 150 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { 151 | inset?: boolean; 152 | }) { 153 | return ( 154 | <DropdownMenuPrimitive.Label 155 | data-slot="dropdown-menu-label" 156 | data-inset={inset} 157 | className={cn( 158 | "px-2 py-1.5 text-sm font-medium data-inset:pl-8", 159 | className, 160 | )} 161 | {...props} 162 | /> 163 | ); 164 | } 165 | 166 | function DropdownMenuSeparator({ 167 | className, 168 | ...props 169 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { 170 | return ( 171 | <DropdownMenuPrimitive.Separator 172 | data-slot="dropdown-menu-separator" 173 | className={cn("bg-border -mx-1 my-1 h-px", className)} 174 | {...props} 175 | /> 176 | ); 177 | } 178 | 179 | function DropdownMenuShortcut({ 180 | className, 181 | ...props 182 | }: React.ComponentProps<"span">) { 183 | return ( 184 | <span 185 | data-slot="dropdown-menu-shortcut" 186 | className={cn( 187 | "text-muted-foreground ml-auto text-xs tracking-widest", 188 | className, 189 | )} 190 | {...props} 191 | /> 192 | ); 193 | } 194 | 195 | function DropdownMenuSub({ 196 | ...props 197 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { 198 | return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />; 199 | } 200 | 201 | function DropdownMenuSubTrigger({ 202 | className, 203 | inset, 204 | children, 205 | ...props 206 | }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { 207 | inset?: boolean; 208 | }) { 209 | return ( 210 | <DropdownMenuPrimitive.SubTrigger 211 | data-slot="dropdown-menu-sub-trigger" 212 | data-inset={inset} 213 | className={cn( 214 | "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-inset:pl-8", 215 | className, 216 | )} 217 | {...props} 218 | > 219 | {children} 220 | <ChevronRightIcon className="ml-auto size-4" /> 221 | </DropdownMenuPrimitive.SubTrigger> 222 | ); 223 | } 224 | 225 | function DropdownMenuSubContent({ 226 | className, 227 | ...props 228 | }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { 229 | return ( 230 | <DropdownMenuPrimitive.SubContent 231 | data-slot="dropdown-menu-sub-content" 232 | className={cn( 233 | "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg", 234 | className, 235 | )} 236 | {...props} 237 | /> 238 | ); 239 | } 240 | 241 | export { 242 | DropdownMenu, 243 | DropdownMenuPortal, 244 | DropdownMenuTrigger, 245 | DropdownMenuContent, 246 | DropdownMenuGroup, 247 | DropdownMenuLabel, 248 | DropdownMenuItem, 249 | DropdownMenuCheckboxItem, 250 | DropdownMenuRadioGroup, 251 | DropdownMenuRadioItem, 252 | DropdownMenuSeparator, 253 | DropdownMenuShortcut, 254 | DropdownMenuSub, 255 | DropdownMenuSubTrigger, 256 | DropdownMenuSubContent, 257 | }; 258 | ```