This is page 32 of 52. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ ├── nextjs │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app │ │ │ ├── (auth) │ │ │ │ ├── forget-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── reset-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── sign-in │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── two-factor │ │ │ │ ├── otp │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── accept-invitation │ │ │ │ └── [id] │ │ │ │ ├── invitation-error.tsx │ │ │ │ └── page.tsx │ │ │ ├── admin │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── auth │ │ │ │ └── [...all] │ │ │ │ └── route.ts │ │ │ ├── apps │ │ │ │ └── register │ │ │ │ └── page.tsx │ │ │ ├── client-test │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ ├── change-plan.tsx │ │ │ │ ├── client.tsx │ │ │ │ ├── organization-card.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── upgrade-button.tsx │ │ │ │ └── user-card.tsx │ │ │ ├── device │ │ │ │ ├── approve │ │ │ │ │ └── page.tsx │ │ │ │ ├── denied │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── success │ │ │ │ └── page.tsx │ │ │ ├── favicon.ico │ │ │ ├── features.tsx │ │ │ ├── fonts │ │ │ │ ├── GeistMonoVF.woff │ │ │ │ └── GeistVF.woff │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── oauth │ │ │ │ └── authorize │ │ │ │ ├── concet-buttons.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── pricing │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── account-switch.tsx │ │ │ ├── blocks │ │ │ │ └── pricing.tsx │ │ │ ├── logo.tsx │ │ │ ├── one-tap.tsx │ │ │ ├── sign-in-btn.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── theme-toggle.tsx │ │ │ ├── tier-labels.tsx │ │ │ ├── ui │ │ │ │ ├── accordion.tsx │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── breadcrumb.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── calendar.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── carousel.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── copy-button.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── form.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── input-otp.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── menubar.tsx │ │ │ │ ├── navigation-menu.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── password-input.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── resizable.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── tabs2.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── toast.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ └── wrapper.tsx │ │ ├── components.json │ │ ├── hooks │ │ │ └── use-toast.ts │ │ ├── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth-types.ts │ │ │ ├── auth.ts │ │ │ ├── email │ │ │ │ ├── invitation.tsx │ │ │ │ ├── resend.ts │ │ │ │ └── reset-password.tsx │ │ │ ├── metadata.ts │ │ │ ├── shared.ts │ │ │ └── utils.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── proxy.ts │ │ ├── public │ │ │ ├── __og.png │ │ │ ├── _og.png │ │ │ ├── favicon │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── light │ │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ │ ├── apple-touch-icon.png │ │ │ │ │ ├── favicon-16x16.png │ │ │ │ │ ├── favicon-32x32.png │ │ │ │ │ ├── favicon.ico │ │ │ │ │ └── site.webmanifest │ │ │ │ └── site.webmanifest │ │ │ ├── logo.svg │ │ │ └── og.png │ │ ├── README.md │ │ ├── tailwind.config.ts │ │ ├── tsconfig.json │ │ └── turbo.json │ └── stateless │ ├── .env.example │ ├── .gitignore │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── src │ │ ├── app │ │ │ ├── api │ │ │ │ ├── auth │ │ │ │ │ └── [...all] │ │ │ │ │ └── route.ts │ │ │ │ └── user │ │ │ │ └── route.ts │ │ │ ├── dashboard │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── lib │ │ ├── auth-client.ts │ │ └── auth.ts │ ├── tailwind.config.ts │ └── tsconfig.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 │ │ │ ├── polar.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 │ ├── 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-custom-schema.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-schema.test.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 │ │ │ │ ├── polar.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 │ │ │ └── index.ts │ │ ├── test │ │ │ └── expo.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.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 -------------------------------------------------------------------------------- /docs/content/docs/concepts/plugins.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Plugins description: Learn how to use plugins with Better Auth. --- Plugins are a key part of Better Auth, they let you extend the base functionalities. You can use them to add new authentication methods, features, or customize behaviors. Better Auth comes with many built-in plugins ready to use. Check the plugins section for details. You can also create your own plugins. ## Using a Plugin Plugins can be a server-side plugin, a client-side plugin, or both. To add a plugin on the server, include it in the `plugins` array in your auth configuration. The plugin will initialize with the provided options. ```ts title="server.ts" import { betterAuth } from "better-auth"; export const auth = betterAuth({ plugins: [ // Add your plugins here ] }); ``` Client plugins are added when creating the client. Most plugin require both server and client plugins to work correctly. The Better Auth auth client on the frontend uses the `createAuthClient` function provided by `better-auth/client`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; const authClient = createAuthClient({ plugins: [ // Add your client plugins here ] }); ``` We recommend keeping the auth-client and your normal auth instance in separate files. <Files> <Folder name="auth" defaultOpen> <File name="server.ts" /> <File name="auth-client.ts" /> </Folder> </Files> ## Creating a Plugin To get started, you'll need a server plugin. Server plugins are the backbone of all plugins, and client plugins are there to provide an interface with frontend APIs to easily work with your server plugins. <Callout type="info"> If your server plugins has endpoints that needs to be called from the client, you'll also need to create a client plugin. </Callout> ### What can a plugin do? * Create custom `endpoint`s to perform any action you want. * Extend database tables with custom `schemas`. * Use a `middleware` to target a group of routes using it's route matcher, and run only when those routes are called through a request. * Use `hooks` to target a specific route or request. And if you want to run the hook even if the endpoint is called directly. * Use `onRequest` or `onResponse` if you want to do something that affects all requests or responses. * Create custom `rate-limit` rule. ## Create a Server plugin To create a server plugin you need to pass an object that satisfies the `BetterAuthPlugin` interface. The only required property is `id`, which is a unique identifier for the plugin. Both server and client plugins can use the same `id`. ```ts title="plugin.ts" import type { BetterAuthPlugin } from "better-auth"; export const myPlugin = ()=>{ return { id: "my-plugin", } satisfies BetterAuthPlugin } ``` <Callout> You don't have to make the plugin a function, but it's recommended to do so. This way you can pass options to the plugin and it's consistent with the built-in plugins. </Callout> ### Endpoints To add endpoints to the server, you can pass `endpoints` which requires an object with the key being any `string` and the value being an `AuthEndpoint`. To create an Auth Endpoint you'll need to import `createAuthEndpoint` from `better-auth`. Better Auth uses wraps around another library called <Link href="https://github.com/bekacru/better-call"> Better Call </Link> to create endpoints. Better call is a simple ts web framework made by the same team behind Better Auth. ```ts title="plugin.ts" import { createAuthEndpoint } from "better-auth/api"; const myPlugin = ()=> { return { id: "my-plugin", endpoints: { getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", { method: "GET", }, async(ctx) => { return ctx.json({ message: "Hello World" }) }) } } satisfies BetterAuthPlugin } ``` Create Auth endpoints wraps around `createEndpoint` from Better Call. Inside the `ctx` object, it'll provide another object called `context` that give you access better-auth specific contexts including `options`, `db`, `baseURL` and more. **Context Object** - `appName`: The name of the application. Defaults to "Better Auth". - `options`: The options passed to the Better Auth instance. - `tables`: Core tables definition. It is an object which has the table name as the key and the schema definition as the value. - `baseURL`: the baseURL of the auth server. This includes the path. For example, if the server is running on `http://localhost:3000`, the baseURL will be `http://localhost:3000/api/auth` by default unless changed by the user. - `session`: The session configuration. Includes `updateAge` and `expiresIn` values. - `secret`: The secret key used for various purposes. This is defined by the user. - `authCookie`: The default cookie configuration for core auth cookies. - `logger`: The logger instance used by Better Auth. - `db`: The Kysely instance used by Better Auth to interact with the database. - `adapter`: This is the same as db but it give you `orm` like functions to interact with the database. (we recommend using this over `db` unless you need raw sql queries or for performance reasons) - `internalAdapter`: These are internal db calls that are used by Better Auth. For example, you can use these calls to create a session instead of using `adapter` directly. `internalAdapter.createSession(userId)` - `createAuthCookie`: This is a helper function that lets you get a cookie `name` and `options` for either to `set` or `get` cookies. It implements things like `__Secure-` prefix for cookies based on whether the connection is secure (HTTPS) or the application is running in production mode. For other properties, you can check the <Link href="https://github.com/bekacru/better-call">Better Call</Link> documentation and the <Link href="https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/init.ts">source code </Link>. **Rules for Endpoints** - Makes sure you use kebab-case for the endpoint path - Make sure to only use `POST` or `GET` methods for the endpoints. - Any function that modifies a data should be a `POST` method. - Any function that fetches data should be a `GET` method. - Make sure to use the `createAuthEndpoint` function to create API endpoints. - Make sure your paths are unique to avoid conflicts with other plugins. If you're using a common path, add the plugin name as a prefix to the path. (`/my-plugin/hello-world` instead of `/hello-world`.) ### Schema You can define a database schema for your plugin by passing a `schema` object. The schema object should have the table name as the key and the schema definition as the value. ```ts title="plugin.ts" import { BetterAuthPlugin } from "better-auth/plugins"; const myPlugin = ()=> { return { id: "my-plugin", schema: { myTable: { fields: { name: { type: "string" } }, modelName: "myTable" // optional if you want to use a different name than the key } } } satisfies BetterAuthPlugin } ``` **Fields** By default better-auth will create an `id` field for each table. You can add additional fields to the table by adding them to the `fields` object. The key is the column name and the value is the column definition. The column definition can have the following properties: `type`: The type of the field. It can be `string`, `number`, `boolean`, `date`. `required`: if the field should be required on a new record. (default: `false`) `unique`: if the field should be unique. (default: `false`) `reference`: if the field is a reference to another table. (default: `null`) It takes an object with the following properties: - `model`: The table name to reference. - `field`: The field name to reference. - `onDelete`: The action to take when the referenced record is deleted. (default: `null`) **Other Schema Properties** `disableMigration`: if the table should not be migrated. (default: `false`) ```ts title="plugin.ts" const myPlugin = (opts: PluginOptions)=>{ return { id: "my-plugin", schema: { rateLimit: { fields: { key: { type: "string", }, }, disableMigration: opts.storage.provider !== "database", // [!code highlight] }, }, } satisfies BetterAuthPlugin } ``` if you add additional fields to a `user` or `session` table, the types will be inferred automatically on `getSession` and `signUpEmail` calls. ```ts title="plugin.ts" const myPlugin = ()=>{ return { id: "my-plugin", schema: { user: { fields: { age: { type: "number", }, }, }, }, } satisfies BetterAuthPlugin } ``` This will add an `age` field to the `user` table and all `user` returning endpoints will include the `age` field and it'll be inferred properly by typescript. <Callout type="warn"> Don't store sensitive information in `user` or `session` table. Crate a new table if you need to store sensitive information. </Callout> ### Hooks Hooks are used to run code before or after an action is performed, either from a client or directly on the server. You can add hooks to the server by passing a `hooks` object, which should contain `before` and `after` properties. ```ts title="plugin.ts" import { createAuthMiddleware } from "better-auth/plugins"; const myPlugin = ()=>{ return { id: "my-plugin", hooks: { before: [{ matcher: (context)=>{ return context.headers.get("x-my-header") === "my-value" }, handler: createAuthMiddleware(async (ctx)=>{ //do something before the request return { context: ctx // if you want to modify the context } }) }], after: [{ matcher: (context)=>{ return context.path === "/sign-up/email" }, handler: createAuthMiddleware(async (ctx)=>{ return ctx.json({ message: "Hello World" }) // if you want to modify the response }) }] } } satisfies BetterAuthPlugin } ``` ### Middleware You can add middleware to the server by passing a `middlewares` array. This array should contain middleware objects, each with a `path` and a `middleware` property. Unlike hooks, middleware only runs on `api` requests from a client. If the endpoint is invoked directly, the middleware will not run. The `path` can be either a string or a path matcher, using the same path-matching system as `better-call`. If you throw an `APIError` from the middleware or returned a `Response` object, the request will be stopped and the response will be sent to the client. ```ts title="plugin.ts" const myPlugin = ()=>{ return { id: "my-plugin", middlewares: [ { path: "/my-plugin/hello-world", middleware: createAuthMiddleware(async(ctx)=>{ //do something }) } ] } satisfies BetterAuthPlugin } ``` ### On Request & On Response Additional to middlewares, you can also hook into right before a request is made and right after a response is returned. This is mostly useful if you want to do something that affects all requests or responses. #### On Request The `onRequest` function is called right before the request is made. It takes two parameters: the `request` and the `context` object. Here’s how it works: - **Continue as Normal**: If you don't return anything, the request will proceed as usual. - **Interrupt the Request**: To stop the request and send a response, return an object with a `response` property that contains a `Response` object. - **Modify the Request**: You can also return a modified `request` object to change the request before it's sent. ```ts title="plugin.ts" const myPlugin = ()=> { return { id: "my-plugin", onRequest: async (request, context) => { //do something }, } satisfies BetterAuthPlugin } ``` #### On Response The `onResponse` function is executed immediately after a response is returned. It takes two parameters: the `response` and the `context` object. Here’s how to use it: - **Modify the Response**: You can return a modified response object to change the response before it is sent to the client. - **Continue Normally**: If you don't return anything, the response will be sent as is. ```ts title="plugin.ts" const myPlugin = ()=>{ return { id: "my-plugin", onResponse: async (response, context) => { //do something }, } satisfies BetterAuthPlugin } ``` ### Rate Limit You can define custom rate limit rules for your plugin by passing a `rateLimit` array. The rate limit array should contain an array of rate limit objects. ```ts title="plugin.ts" const myPlugin = ()=>{ return { id: "my-plugin", rateLimit: [ { pathMatcher: (path)=>{ return path === "/my-plugin/hello-world" }, limit: 10, window: 60, } ] } satisfies BetterAuthPlugin } ``` ### Server-plugin helper functions Some additional helper functions for creating server plugins. #### `getSessionFromCtx` Allows you to get the client's session data by passing the auth middleware's `context`. ```ts title="plugin.ts" import { createAuthMiddleware } from "better-auth/plugins"; import { getSessionFromCtx } from "better-auth/api"; const myPlugin = { id: "my-plugin", hooks: { before: [{ matcher: (context)=>{ return context.headers.get("x-my-header") === "my-value" }, handler: createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); //do something with the client's session. return { context: ctx } }) }], } } satisfies BetterAuthPlugin ``` #### `sessionMiddleware` A middleware that checks if the client has a valid session. If the client has a valid session, it'll add the session data to the context object. ```ts title="plugin.ts" import { createAuthMiddleware } from "better-auth/plugins"; import { sessionMiddleware } from "better-auth/api"; const myPlugin = ()=>{ return { id: "my-plugin", endpoints: { getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", { method: "GET", use: [sessionMiddleware], // [!code highlight] }, async(ctx) => { const session = ctx.context.session; return ctx.json({ message: "Hello World" }) }) } } satisfies BetterAuthPlugin } ``` ## Creating a client plugin If your endpoints needs to be called from the client, you'll need to also create a client plugin. Better Auth clients can infer the endpoints from the server plugins. You can also add additional client side logic. ```ts title="client-plugin.ts" import type { BetterAuthClientPlugin } from "better-auth"; export const myPluginClient = ()=>{ return { id: "my-plugin", } satisfies BetterAuthClientPlugin } ``` ### Endpoint Interface Endpoints are inferred from the server plugin by adding a `$InferServerPlugin` key to the client plugin. The client infers the `path` as an object and converts kebab-case to camelCase. For example, `/my-plugin/hello-world` becomes `myPlugin.helloWorld`. ```ts title="client-plugin.ts" import type { BetterAuthClientPlugin } from "better-auth/client"; import type { myPlugin } from "./plugin"; const myPluginClient = ()=> { return { id: "my-plugin", $InferServerPlugin: {} as ReturnType<typeof myPlugin>, } satisfies BetterAuthClientPlugin } ``` ### Get actions If you need to add additional methods or what not to the client you can use the `getActions` function. This function is called with the `fetch` function from the client. Better Auth uses <Link href="https://better-fetch.vercel.app"> Better fetch </Link> to make requests. Better fetch is a simple fetch wrapper made by the same author of Better Auth. ```ts title="client-plugin.ts" import type { BetterAuthClientPlugin } from "better-auth/client"; import type { myPlugin } from "./plugin"; import type { BetterFetchOption } from "@better-fetch/fetch"; const myPluginClient = { id: "my-plugin", $InferServerPlugin: {} as ReturnType<typeof myPlugin>, getActions: ($fetch)=>{ return { myCustomAction: async (data: { foo: string, }, fetchOptions?: BetterFetchOption)=>{ const res = $fetch("/custom/action", { method: "POST", body: { foo: data.foo }, ...fetchOptions }) return res } } } } satisfies BetterAuthClientPlugin ``` <Callout> As a general guideline, ensure that each function accepts only one argument, with an optional second argument for fetchOptions to allow users to pass additional options to the fetch call. The function should return an object containing data and error keys. If your use case involves actions beyond API calls, feel free to deviate from this rule. </Callout> ### Get Atoms This is only useful if you want to provide `hooks` like `useSession`. Get atoms is called with the `fetch` function from better fetch and it should return an object with the atoms. The atoms should be created using <Link href="https://github.com/nanostores/nanostores">nanostores</Link>. The atoms will be resolved by each framework `useStore` hook provided by nanostores. ```ts title="client-plugin.ts" import { atom } from "nanostores"; import type { BetterAuthClientPlugin } from "better-auth/client"; const myPluginClient = { id: "my-plugin", $InferServerPlugin: {} as ReturnType<typeof myPlugin>, getAtoms: ($fetch)=>{ const myAtom = atom<null>() return { myAtom } } } satisfies BetterAuthClientPlugin ``` See built-in plugins for examples of how to use atoms properly. ### Path methods By default, inferred paths use `GET` method if they don't require a body and `POST` if they do. You can override this by passing a `pathMethods` object. The key should be the path and the value should be the method ("POST" | "GET"). ```ts title="client-plugin.ts" import type { BetterAuthClientPlugin } from "better-auth/client"; import type { myPlugin } from "./plugin"; const myPluginClient = { id: "my-plugin", $InferServerPlugin: {} as ReturnType<typeof myPlugin>, pathMethods: { "/my-plugin/hello-world": "POST" } } satisfies BetterAuthClientPlugin ``` ### Fetch plugins If you need to use better fetch plugins you can pass them to the `fetchPlugins` array. You can read more about better fetch plugins in the <Link href="https://better-fetch.vercel.app/docs/plugins">better fetch documentation</Link>. ### Atom Listeners This is only useful if you want to provide `hooks` like `useSession` and you want to listen to atoms and re-evaluate them when they change. You can see how this is used in the built-in plugins. ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/admin.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Admin description: Admin plugin for Better Auth --- The Admin plugin provides a set of administrative functions for user management in your application. It allows administrators to perform various operations such as creating users, managing user roles, banning/unbanning users, impersonating users, and more. ## Installation <Steps> <Step> ### Add the plugin to your auth config To use the Admin plugin, add it to your auth config. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { admin } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ // ... other config options plugins: [ admin() // [!code highlight] ] }) ``` </Step> <Step> ### Migrate the database Run the migration or generate the schema to add the necessary fields and tables to the database. <Tabs items={["migrate", "generate"]}> <Tab value="migrate"> ```bash npx @better-auth/cli migrate ``` </Tab> <Tab value="generate"> ```bash npx @better-auth/cli generate ``` </Tab> </Tabs> See the [Schema](#schema) section to add the fields manually. </Step> <Step> ### Add the client plugin Next, include the admin client plugin in your authentication client instance. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { adminClient } from "better-auth/client/plugins" export const authClient = createAuthClient({ plugins: [ adminClient() ] }) ``` </Step> </Steps> ## Usage Before performing any admin operations, the user must be authenticated with an admin account. An admin is any user assigned the `admin` role or any user whose ID is included in the `adminUserIds` option. ### Create User Allows an admin to create a new user. <APIMethod path="/admin/create-user" method="POST" resultVariable="newUser" > ```ts type createUser = { /** * The email of the user. */ email: string = "[email protected]" /** * The password of the user. */ password: string = "some-secure-password" /** * The name of the user. */ name: string = "James Smith" /** * A string or array of strings representing the roles to apply to the new user. */ role?: string | string[] = "user" /** * Extra fields for the user. Including custom additional fields. */ data?: Record<string, any> = { customField: "customValue" } } ``` </APIMethod> ### List Users Allows an admin to list all users in the database. <APIMethod path="/admin/list-users" method="GET" requireSession note={"All properties are optional to configure. By default, 100 rows are returned, you can configure this by the `limit` property."} resultVariable={"users"} > ```ts type listUsers = { /** * The value to search for. */ searchValue?: string = "some name" /** * The field to search in, defaults to email. Can be `email` or `name`. */ searchField?: "email" | "name" = "name" /** * The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. */ searchOperator?: "contains" | "starts_with" | "ends_with" = "contains" /** * The number of users to return. Defaults to 100. */ limit?: string | number = 100 /** * The offset to start from. */ offset?: string | number = 100 /** * The field to sort by. */ sortBy?: string = "name" /** * The direction to sort by. */ sortDirection?: "asc" | "desc" = "desc" /** * The field to filter by. */ filterField?: string = "email" /** * The value to filter by. */ filterValue?: string | number | boolean = "[email protected]" /** * The operator to use for the filter. */ filterOperator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" = "eq" } ``` </APIMethod> #### Query Filtering The `listUsers` function supports various filter operators including `eq`, `contains`, `starts_with`, and `ends_with`. #### Pagination The `listUsers` function supports pagination by returning metadata alongside the user list. The response includes the following fields: ```ts { users: User[], // Array of returned users total: number, // Total number of users after filters and search queries limit: number | undefined, // The limit provided in the query offset: number | undefined // The offset provided in the query } ``` ##### How to Implement Pagination To paginate results, use the `total`, `limit`, and `offset` values to calculate: - **Total pages:** `Math.ceil(total / limit)` - **Current page:** `(offset / limit) + 1` - **Next page offset:** `Math.min(offset + limit, (total - 1))` – The value to use as `offset` for the next page, ensuring it does not exceed the total number of pages. - **Previous page offset:** `Math.max(0, offset - limit)` – The value to use as `offset` for the previous page (ensuring it doesn’t go below zero). ##### Example Usage Fetching the second page with 10 users per page: ```ts title="admin.ts" const pageSize = 10; const currentPage = 2; const users = await authClient.admin.listUsers({ query: { limit: pageSize, offset: (currentPage - 1) * pageSize } }); const totalUsers = users.total; const totalPages = Math.ceil(totalUsers / pageSize) ``` ### Set User Role Changes the role of a user. <APIMethod path="/admin/set-role" method="POST" requireSession > ```ts type setRole = { /** * The user id which you want to set the role for. */ userId?: string = "user-id" /** * The role to set, this can be a string or an array of strings. */ role: string | string[] = "admin" } ``` </APIMethod> ### Set User Password Changes the password of a user. <APIMethod path="/admin/set-user-password" method="POST" requireSession > ```ts type setUserPassword = { /** * The new password. */ newPassword: string = 'new-password' /** * The user id which you want to set the password for. */ userId: string = 'user-id' } ``` </APIMethod> ### Update user Update a user's details. <APIMethod path="/admin/update-user" method="POST" requireSession > ```ts type adminUpdateUser = { /** * The user id which you want to update. */ userId: string = "user-id" /** * The data to update. */ data: Record<string, any> = { name: "John Doe" } } ``` </APIMethod> ### Ban User Bans a user, preventing them from signing in and revokes all of their existing sessions. <APIMethod path="/admin/ban-user" method="POST" requireSession noResult > ```ts type banUser = { /** * The user id which you want to ban. */ userId: string = "user-id" /** * The reason for the ban. */ banReason?: string = "Spamming" /** * The number of seconds until the ban expires. If not provided, the ban will never expire. */ banExpiresIn?: number = 60 * 60 * 24 * 7 } ``` </APIMethod> ### Unban User Removes the ban from a user, allowing them to sign in again. <APIMethod path="/admin/unban-user" method="POST" requireSession noResult > ```ts type unbanUser = { /** * The user id which you want to unban. */ userId: string = "user-id" } ``` </APIMethod> ### List User Sessions Lists all sessions for a user. <APIMethod path="/admin/list-user-sessions" method="POST" requireSession > ```ts type listUserSessions = { /** * The user id. */ userId: string = "user-id" } ``` </APIMethod> ### Revoke User Session Revokes a specific session for a user. <APIMethod path="/admin/revoke-user-session" method="POST" requireSession > ```ts type revokeUserSession = { /** * The session token which you want to revoke. */ sessionToken: string = "session_token_here" } ``` </APIMethod> ### Revoke All Sessions for a User Revokes all sessions for a user. <APIMethod path="/admin/revoke-user-sessions" method="POST" requireSession > ```ts type revokeUserSessions = { /** * The user id which you want to revoke all sessions for. */ userId: string = "user-id" } ``` </APIMethod> ### Impersonate User This feature allows an admin to create a session that mimics the specified user. The session will remain active until either the browser session ends or it reaches 1 hour. You can change this duration by setting the `impersonationSessionDuration` option. <APIMethod path="/admin/impersonate-user" method="POST" requireSession > ```ts type impersonateUser = { /** * The user id which you want to impersonate. */ userId: string = "user-id" } ``` </APIMethod> ### Stop Impersonating User To stop impersonating a user and continue with the admin account, you can use `stopImpersonating` <APIMethod path="/admin/stop-impersonating" method="POST" noResult requireSession> ```ts type stopImpersonating = { } ``` </APIMethod> ### Remove User Hard deletes a user from the database. <APIMethod path="/admin/remove-user" method="POST" requireSession resultVariable="deletedUser" > ```ts type removeUser = { /** * The user id which you want to remove. */ userId: string = "user-id" } ``` </APIMethod> ## Access Control The admin plugin offers a highly flexible access control system, allowing you to manage user permissions based on their role. You can define custom permission sets to fit your needs. ### Roles By default, there are two roles: `admin`: Users with the admin role have full control over other users. `user`: Users with the user role have no control over other users. <Callout> A user can have multiple roles. Multiple roles are stored as string separated by comma (","). </Callout> ### Permissions By default, there are two resources with up to six permissions. **user**: `create` `list` `set-role` `ban` `impersonate` `delete` `set-password` **session**: `list` `revoke` `delete` Users with the admin role have full control over all the resources and actions. Users with the user role have no control over any of those actions. ### Custom Permissions The plugin provides an easy way to define your own set of permissions for each role. <Steps> <Step> #### Create Access Control You first need to create an access controller by calling the `createAccessControl` function and passing the statement object. The statement object should have the resource name as the key and the array of actions as the value. ```ts title="permissions.ts" import { createAccessControl } from "better-auth/plugins/access"; /** * make sure to use `as const` so typescript can infer the type correctly */ const statement = { // [!code highlight] project: ["create", "share", "update", "delete"], // [!code highlight] } as const; // [!code highlight] const ac = createAccessControl(statement); // [!code highlight] ``` </Step> <Step> #### Create Roles Once you have created the access controller you can create roles with the permissions you have defined. ```ts title="permissions.ts" import { createAccessControl } from "better-auth/plugins/access"; export const statement = { project: ["create", "share", "update", "delete"], // <-- Permissions available for created roles } as const; const ac = createAccessControl(statement); export const user = ac.newRole({ // [!code highlight] project: ["create"], // [!code highlight] }); // [!code highlight] export const admin = ac.newRole({ // [!code highlight] project: ["create", "update"], // [!code highlight] }); // [!code highlight] export const myCustomRole = ac.newRole({ // [!code highlight] project: ["create", "update", "delete"], // [!code highlight] user: ["ban"], // [!code highlight] }); // [!code highlight] ``` When you create custom roles for existing roles, the predefined permissions for those roles will be overridden. To add the existing permissions to the custom role, you need to import `defaultStatements` and merge it with your new statement, plus merge the roles' permissions set with the default roles. ```ts title="permissions.ts" import { createAccessControl } from "better-auth/plugins/access"; import { defaultStatements, adminAc } from "better-auth/plugins/admin/access"; const statement = { ...defaultStatements, // [!code highlight] project: ["create", "share", "update", "delete"], } as const; const ac = createAccessControl(statement); const admin = ac.newRole({ project: ["create", "update"], ...adminAc.statements, // [!code highlight] }); ``` </Step> <Step> #### Pass Roles to the Plugin Once you have created the roles you can pass them to the admin plugin both on the client and the server. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { admin as adminPlugin } from "better-auth/plugins" import { ac, admin, user } from "@/auth/permissions" export const auth = betterAuth({ plugins: [ adminPlugin({ ac, roles: { admin, user, myCustomRole } }), ], }); ``` You also need to pass the access controller and the roles to the client plugin. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { adminClient } from "better-auth/client/plugins" import { ac, admin, user, myCustomRole } from "@/auth/permissions" export const client = createAuthClient({ plugins: [ adminClient({ ac, roles: { admin, user, myCustomRole } }) ] }) ``` </Step> </Steps> ### Access Control Usage **Has Permission**: To check a user's permissions, you can use the `hasPermission` function provided by the client. <APIMethod path="/admin/has-permission" method="POST"> ```ts type userHasPermission = { /** * The user id which you want to check the permissions for. */ userId?: string = "user-id" /** * Check role permissions. * @serverOnly */ role?: string = "admin" /** * Optionally check if a single permission is granted. Must use this, or permissions. */ permission?: Record<string, string[]> = { "project": ["create", "update"] } /* Must use this, or permissions */, /** * Optionally check if multiple permissions are granted. Must use this, or permission. */ permissions?: Record<string, string[]> } ``` </APIMethod> Example usage: ```ts title="auth-client.ts" const canCreateProject = await authClient.admin.hasPermission({ permissions: { project: ["create"], }, }); // You can also check multiple resource permissions at the same time const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({ permissions: { project: ["create"], sale: ["create"] }, }); ``` If you want to check a user's permissions server-side, you can use the `userHasPermission` action provided by the `api` to check the user's permissions. ```ts title="api.ts" import { auth } from "@/auth"; await auth.api.userHasPermission({ body: { userId: 'id', //the user id permissions: { project: ["create"], // This must match the structure in your access control }, }, }); // You can also just pass the role directly await auth.api.userHasPermission({ body: { role: "admin", permissions: { project: ["create"], // This must match the structure in your access control }, }, }); // You can also check multiple resource permissions at the same time await auth.api.userHasPermission({ body: { role: "admin", permissions: { project: ["create"], // This must match the structure in your access control sale: ["create"] }, }, }); ``` **Check Role Permission**: Use the `checkRolePermission` function on the client side to verify whether a given **role** has a specific **permission**. This is helpful after defining roles and their permissions, as it allows you to perform permission checks without needing to contact the server. Note that this function does **not** check the permissions of the currently logged-in user directly. Instead, it checks what permissions are assigned to a specified role. The function is synchronous, so you don't need to use `await` when calling it. ```ts title="auth-client.ts" const canCreateProject = authClient.admin.checkRolePermission({ permissions: { user: ["delete"], }, role: "admin", }); // You can also check multiple resource permissions at the same time const canDeleteUserAndRevokeSession = authClient.admin.checkRolePermission({ permissions: { user: ["delete"], session: ["revoke"] }, role: "admin", }); ``` ## Schema This plugin adds the following fields to the `user` table: <DatabaseTable fields={[ { name: "role", type: "string", description: "The user's role. Defaults to `user`. Admins will have the `admin` role.", isOptional: true, }, { name: "banned", type: "boolean", description: "Indicates whether the user is banned.", isOptional: true, }, { name: "banReason", type: "string", description: "The reason for the user's ban.", isOptional: true, }, { name: "banExpires", type: "date", description: "The date when the user's ban will expire.", isOptional: true, }, ]} /> And adds one field in the `session` table: <DatabaseTable fields={[ { name: "impersonatedBy", type: "string", description: "The ID of the admin that is impersonating this session.", isOptional: true, }, ]} /> ## Options ### Default Role The default role for a user. Defaults to `user`. ```ts title="auth.ts" admin({ defaultRole: "regular", }); ``` ### Admin Roles The roles that are considered admin roles. Defaults to `["admin"]`. ```ts title="auth.ts" admin({ adminRoles: ["admin", "superadmin"], }); ``` <Callout type="warning"> Any role that isn't in the `adminRoles` list, even if they have the permission, will not be considered an admin. </Callout> ### Admin userIds You can pass an array of userIds that should be considered as admin. Default to `[]` ```ts title="auth.ts" admin({ adminUserIds: ["user_id_1", "user_id_2"] }) ``` If a user is in the `adminUserIds` list, they will be able to perform any admin operation. ### impersonationSessionDuration The duration of the impersonation session in seconds. Defaults to 1 hour. ```ts title="auth.ts" admin({ impersonationSessionDuration: 60 * 60 * 24, // 1 day }); ``` ### Default Ban Reason The default ban reason for a user created by the admin. Defaults to `No reason`. ```ts title="auth.ts" admin({ defaultBanReason: "Spamming", }); ``` ### Default Ban Expires In The default ban expires in for a user created by the admin in seconds. Defaults to `undefined` (meaning the ban never expires). ```ts title="auth.ts" admin({ defaultBanExpiresIn: 60 * 60 * 24, // 1 day }); ``` ### bannedUserMessage The message to show when a banned user tries to sign in. Defaults to "You have been banned from this application. Please contact support if you believe this is an error." ```ts title="auth.ts" admin({ bannedUserMessage: "Custom banned user message", }); ``` ``` -------------------------------------------------------------------------------- /docs/components/ui/sidebar.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { VariantProps, cva } from "class-variance-authority"; import { PanelLeftIcon } from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_KEYBOARD_SHORTCUT = "b"; type SidebarContext = { state: "expanded" | "collapsed"; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; }; const SidebarContext = React.createContext<SidebarContext | null>(null); function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error("useSidebar must be used within a SidebarProvider."); } return context; } function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }: React.ComponentProps<"div"> & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; }) { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = React.useState(defaultOpen); const open = openProp ?? _open; const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { const openState = typeof value === "function" ? value(open) : value; if (setOpenProp) { setOpenProp(openState); } else { _setOpen(openState); } // This sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, [setOpenProp, open], ); // Helper to toggle the sidebar. const toggleSidebar = React.useCallback(() => { return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); }, [isMobile, setOpen, setOpenMobile]); // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ( event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey) ) { event.preventDefault(); toggleSidebar(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [toggleSidebar]); // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. const state = open ? "expanded" : "collapsed"; const contextValue = React.useMemo<SidebarContext>( () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], ); return ( <SidebarContext.Provider value={contextValue}> <TooltipProvider delayDuration={0}> <div data-slot="sidebar-wrapper" style={ { "--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, ...style, } as React.CSSProperties } className={cn( "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", className, )} {...props} > {children} </div> </TooltipProvider> </SidebarContext.Provider> ); } function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }: React.ComponentProps<"div"> & { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; }) { const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === "none") { return ( <div data-slot="sidebar" className={cn( "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", className, )} {...props} > {children} </div> ); } if (isMobile) { return ( <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> <SheetContent data-sidebar="sidebar" data-slot="sidebar" data-mobile="true" className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" style={ { "--sidebar-width": SIDEBAR_WIDTH_MOBILE, } as React.CSSProperties } side={side} > <SheetHeader className="sr-only"> <SheetTitle>Sidebar</SheetTitle> <SheetDescription>Displays the mobile sidebar.</SheetDescription> </SheetHeader> <div className="flex h-full w-full flex-col">{children}</div> </SheetContent> </Sheet> ); } return ( <div className="group peer text-sidebar-foreground hidden md:block" data-state={state} data-collapsible={state === "collapsed" ? collapsible : ""} data-variant={variant} data-side={side} data-slot="sidebar" > {/* This is what handles the sidebar gap on desktop */} <div className={cn( "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", "group-data-[collapsible=offcanvas]:w-0", "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", )} /> <div className={cn( "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", side === "left" ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", // Adjust the padding for floating and inset variants. variant === "floating" || variant === "inset" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", className, )} {...props} > <div data-sidebar="sidebar" className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" > {children} </div> </div> </div> ); } function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) { const { toggleSidebar } = useSidebar(); return ( <Button data-sidebar="trigger" data-slot="sidebar-trigger" variant="ghost" size="icon" className={cn("h-7 w-7", className)} onClick={(event) => { onClick?.(event); toggleSidebar(); }} {...props} > <PanelLeftIcon /> <span className="sr-only">Toggle Sidebar</span> </Button> ); } function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { const { toggleSidebar } = useSidebar(); return ( <button data-sidebar="rail" data-slot="sidebar-rail" aria-label="Toggle Sidebar" tabIndex={-1} onClick={toggleSidebar} title="Toggle Sidebar" className={cn( "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className, )} {...props} /> ); } function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { return ( <main data-slot="sidebar-inset" className={cn( "bg-background relative flex w-full flex-1 flex-col", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", className, )} {...props} /> ); } function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) { return ( <Input data-slot="sidebar-input" data-sidebar="input" className={cn("bg-background h-8 w-full shadow-none", className)} {...props} /> ); } function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-header" data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} /> ); } function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-footer" data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} /> ); } function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) { return ( <Separator data-slot="sidebar-separator" data-sidebar="separator" className={cn("bg-sidebar-border mx-2 w-auto", className)} {...props} /> ); } function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-content" data-sidebar="content" className={cn( "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", className, )} {...props} /> ); } function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-group" data-sidebar="group" className={cn("relative flex w-full min-w-0 flex-col p-2", className)} {...props} /> ); } function SidebarGroupLabel({ className, asChild = false, ...props }: React.ComponentProps<"div"> & { asChild?: boolean }) { const Comp = asChild ? Slot : "div"; return ( <Comp data-slot="sidebar-group-label" data-sidebar="group-label" className={cn( "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", className, )} {...props} /> ); } function SidebarGroupAction({ className, asChild = false, ...props }: React.ComponentProps<"button"> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="sidebar-group-action" data-sidebar="group-action" className={cn( "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", // Increases the hit area of the button on mobile. "after:absolute after:-inset-2 md:after:hidden", "group-data-[collapsible=icon]:hidden", className, )} {...props} /> ); } function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-group-content" data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} /> ); } function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { return ( <ul data-slot="sidebar-menu" data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} /> ); } function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { return ( <li data-slot="sidebar-menu-item" data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} /> ); } const sidebarMenuButtonVariants = cva( "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", { variants: { variant: { default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", outline: "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", }, size: { default: "h-8 text-sm", sm: "h-7 text-xs", lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); function SidebarMenuButton({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }: React.ComponentProps<"button"> & { asChild?: boolean; isActive?: boolean; tooltip?: string | React.ComponentProps<typeof TooltipContent>; } & VariantProps<typeof sidebarMenuButtonVariants>) { const Comp = asChild ? Slot : "button"; const { isMobile, state } = useSidebar(); const button = ( <Comp data-slot="sidebar-menu-button" data-sidebar="menu-button" data-size={size} data-active={isActive} className={cn(sidebarMenuButtonVariants({ variant, size }), className)} {...props} /> ); if (!tooltip) { return button; } if (typeof tooltip === "string") { tooltip = { children: tooltip, }; } return ( <Tooltip> <TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} /> </Tooltip> ); } function SidebarMenuAction({ className, asChild = false, showOnHover = false, ...props }: React.ComponentProps<"button"> & { asChild?: boolean; showOnHover?: boolean; }) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="sidebar-menu-action" data-sidebar="menu-action" className={cn( "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", // Increases the hit area of the button on mobile. "after:absolute after:-inset-2 md:after:hidden", "peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden", showOnHover && "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", className, )} {...props} /> ); } function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-menu-badge" data-sidebar="menu-badge" className={cn( "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", "peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden", className, )} {...props} /> ); } function SidebarMenuSkeleton({ className, showIcon = false, ...props }: React.ComponentProps<"div"> & { showIcon?: boolean; }) { // Random width between 50 to 90%. const width = React.useMemo(() => { return `${Math.floor(Math.random() * 40) + 50}%`; }, []); return ( <div data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} {...props} > {showIcon && ( <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" /> )} <Skeleton className="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" style={ { "--skeleton-width": width, } as React.CSSProperties } /> </div> ); } function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { return ( <ul data-slot="sidebar-menu-sub" data-sidebar="menu-sub" className={cn( "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", "group-data-[collapsible=icon]:hidden", className, )} {...props} /> ); } function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) { return ( <li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" className={cn("group/menu-sub-item relative", className)} {...props} /> ); } function SidebarMenuSubButton({ asChild = false, size = "md", isActive = false, className, ...props }: React.ComponentProps<"a"> & { asChild?: boolean; size?: "sm" | "md"; isActive?: boolean; }) { const Comp = asChild ? Slot : "a"; return ( <Comp data-slot="sidebar-menu-sub-button" data-sidebar="menu-sub-button" data-size={size} data-active={isActive} className={cn( "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", size === "sm" && "text-xs", size === "md" && "text-sm", "group-data-[collapsible=icon]:hidden", className, )} {...props} /> ); } export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, useSidebar, }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/cookies/cookies.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getTestInstance } from "../test-utils/test-instance"; import { getCookieCache, getCookies, getSessionCookie } from "../cookies"; import { parseSetCookieHeader } from "./cookie-utils"; import type { BetterAuthOptions } from "@better-auth/core"; describe("cookies", async () => { const { client, testUser } = await getTestInstance(); it("should set cookies with default options", async () => { await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toBeDefined(); expect(setCookie).toContain("Path=/"); expect(setCookie).toContain("HttpOnly"); expect(setCookie).toContain("SameSite=Lax"); expect(setCookie).toContain("better-auth"); }, }, ); }); it("should set multiple cookies", async () => { await client.signIn.social( { provider: "github", callbackURL: "https://example.com", }, { onSuccess(context) { const cookies = context.response.headers.get("Set-Cookie"); expect(cookies?.split(",").length).toBeGreaterThan(1); }, }, ); }); it("should use secure cookies", async () => { const { client, testUser } = await getTestInstance({ advanced: { useSecureCookies: true }, }); const res = await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toContain("Secure"); }, }, ); }); it("should use secure cookies when the base url is https", async () => { const { client, testUser } = await getTestInstance({ baseURL: "https://example.com", }); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toContain("Secure"); }, }, ); }); }); describe("crossSubdomainCookies", () => { it("should update cookies with custom domain", async () => { const { client, testUser } = await getTestInstance({ advanced: { crossSubDomainCookies: { enabled: true, domain: "example.com", }, }, }); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toContain("Domain=example.com"); expect(setCookie).toContain("SameSite=Lax"); }, }, ); }); it("should use default domain from baseURL if not provided", async () => { const { testUser, client } = await getTestInstance({ baseURL: "https://example.com", advanced: { crossSubDomainCookies: { enabled: true, }, }, }); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toContain("Domain=example.com"); }, }, ); }); }); describe("cookie configuration", () => { it("should return correct cookie options based on configuration", async () => { const options = { baseURL: "https://example.com", database: {} as BetterAuthOptions["database"], advanced: { useSecureCookies: true, crossSubDomainCookies: { enabled: true, domain: "example.com", }, cookiePrefix: "test-prefix", }, } satisfies BetterAuthOptions; const cookies = getCookies(options); expect(cookies.sessionToken.options.secure).toBe(true); expect(cookies.sessionToken.name).toContain("test-prefix.session_token"); expect(cookies.sessionData.options.sameSite).toBe("lax"); expect(cookies.sessionData.options.domain).toBe("example.com"); }); }); describe("cookie-utils parseSetCookieHeader", () => { it("handles Expires with commas and multiple cookies", () => { const header = "a=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/, b=2; Expires=Thu, 22 Oct 2015 07:28:00 GMT; Path=/"; const map = parseSetCookieHeader(header); expect(map.get("a")?.value).toBe("1"); expect(map.get("b")?.value).toBe("2"); }); }); describe("getSessionCookie", async () => { it("should return the correct session cookie", async () => { const { client, testUser, signInWithTestUser } = await getTestInstance(); const { headers } = await signInWithTestUser(); const request = new Request("http://localhost:3000/api/auth/session", { headers, }); const cookies = getSessionCookie(request); expect(cookies).not.toBeNull(); expect(cookies).toBeDefined(); }); it("should return the correct session cookie on production", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ baseURL: "https://example.com", }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cookies = getSessionCookie(request); expect(cookies).not.toBeNull(); expect(cookies).toBeDefined(); }); it("should allow override cookie prefix", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ advanced: { useSecureCookies: true, cookiePrefix: "test-prefix", }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers) }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cookies = getSessionCookie(request, { cookiePrefix: "test-prefix", }); expect(cookies).not.toBeNull(); }); it("should allow override cookie name", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ advanced: { useSecureCookies: true, cookiePrefix: "test", cookies: { session_token: { name: "test-session-token", }, }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cookies = getSessionCookie(request, { cookieName: "session-token", cookiePrefix: "test", }); expect(cookies).not.toBeNull(); }); it("should return cookie cache", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", session: { cookieCache: { enabled: true, }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", }); expect(cache).not.toBeNull(); expect(cache?.user?.email).toEqual(testUser.email); expect(cache?.session?.token).toEqual(expect.any(String)); }); it("should respect dontRememberMe when storing session in cookie cache", async () => { const { client, testUser } = await getTestInstance({ secret: "better-auth.secret", session: { cookieCache: { enabled: true, }, }, }); await client.signIn.email( { email: testUser.email, password: testUser.password, rememberMe: false, }, { onSuccess(c) { const headers = c.response.headers; const setCookieHeader = headers.get("set-cookie"); expect(setCookieHeader).toBeDefined(); const parsed = parseSetCookieHeader(setCookieHeader!); const sessionTokenCookie = parsed.get("better-auth.session_token")!; expect(sessionTokenCookie).toBeDefined(); expect(sessionTokenCookie["max-age"]).toBeUndefined(); const sessionDataCookie = parsed.get("better-auth.session_data")!; expect(sessionDataCookie).toBeDefined(); expect(sessionDataCookie["max-age"]).toBeUndefined(); }, }, ); }); it("should return null if the cookie is invalid", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ session: { cookieCache: { enabled: true, }, }, }); const headers = new Headers(); await client.signIn.email({ email: testUser.email, password: testUser.password, }); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "wrong-secret", }); expect(cache).toBeNull(); }); it("should throw an error if the secret is not provided", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ session: { cookieCache: { enabled: true, }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); await expect(getCookieCache(request)).rejects.toThrow(); }); it("should log error and skip setting cookie when data exceeds size limit", async () => { const loggerErrors: string[] = []; const mockLogger = { log: (level: string, message: string) => { if (level === "error") { loggerErrors.push(message); } }, }; const { auth } = await getTestInstance({ secret: "better-auth.secret", user: { additionalFields: { customField1: { type: "string", defaultValue: "", }, customField2: { type: "string", defaultValue: "", }, customField3: { type: "string", defaultValue: "", }, }, }, session: { cookieCache: { enabled: true, }, }, logger: mockLogger, }); // Create a very large string that will exceed the cookie size limit when combined with session data // The limit is 4093 bytes, so we create data that will definitely exceed it const largeString = "x".repeat(2000); // Sign up with large user data using the server API const result = await auth.api.signUpEmail({ body: { name: "Test User", email: "[email protected]", password: "password123", customField1: largeString, customField2: largeString, customField3: largeString, }, }); // Check that logger recorded an error about exceeding size limit const sizeError = loggerErrors.find((msg) => msg.includes("Session data exceeds cookie size limit"), ); expect(sizeError).toBeDefined(); expect(sizeError).toContain("4093 bytes"); // The sign up should still succeed expect(result).toBeDefined(); expect(result?.user).toBeDefined(); }); }); describe("Cookie Cache Field Filtering", () => { it("should exclude user fields with returned: false from cookie cache", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", user: { additionalFields: { internalNote: { type: "string", defaultValue: "", returned: false, }, }, }, session: { cookieCache: { enabled: true, }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", }); expect(cache).not.toBeNull(); expect(cache?.user?.email).toEqual(testUser.email); expect(cache?.user?.internalNote).toBeUndefined(); }); it("should correctly filter multiple user fields based on returned config", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", user: { additionalFields: { publicBio: { type: "string", defaultValue: "default-bio", returned: true, }, internalNotes: { type: "string", defaultValue: "internal-notes", returned: false, }, preferences: { type: "string", defaultValue: "default-prefs", returned: true, }, adminFlags: { type: "string", defaultValue: "admin-flags", returned: false, }, }, }, session: { cookieCache: { enabled: true, }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", }); expect(cache).not.toBeNull(); // Fields with returned: true should be included expect(cache?.user?.publicBio).toBeDefined(); expect(cache?.user?.preferences).toBeDefined(); // Fields with returned: false should be excluded expect(cache?.user?.internalNotes).toBeUndefined(); expect(cache?.user?.adminFlags).toBeUndefined(); }); it("should reduce cookie size when large fields are excluded", async () => { const largeString = "x".repeat(2000); const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", user: { additionalFields: { largeBio: { type: "string", defaultValue: largeString, returned: false, }, smallField: { type: "string", defaultValue: "small-value", returned: true, }, }, }, session: { cookieCache: { enabled: true, }, }, }); const headers = new Headers(); // Sign in with testUser await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", }); // Cookie cache should exist (not exceed size limit) expect(cache).not.toBeNull(); // Large field should be excluded expect(cache?.user?.largeBio).toBeUndefined(); // Small field should be included expect(cache?.user?.smallField).toBeDefined(); }); it("should maintain session field filtering (regression check)", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", session: { additionalFields: { internalSessionData: { type: "string", defaultValue: "internal-data", returned: false, }, publicSessionData: { type: "string", defaultValue: "public-data", returned: true, }, }, cookieCache: { enabled: true, }, }, }); const headers = new Headers(); // Sign in with testUser await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", }); expect(cache).not.toBeNull(); // Verify session field filtering still works correctly // Session should have token expect(cache?.session?.token).toEqual(expect.any(String)); // Session field with returned: false should be excluded expect(cache?.session?.internalSessionData).toBeUndefined(); }); it("should include unknown user fields for backward compatibility", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", user: { additionalFields: { knownField: { type: "string", defaultValue: "known-value", returned: false, }, }, }, session: { cookieCache: { enabled: true, }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", }); expect(cache).not.toBeNull(); // Known field with returned: false should be excluded expect(cache?.user?.knownField).toBeUndefined(); // Standard fields like email, name should be included (backward compatibility) expect(cache?.user?.email).toEqual(testUser.email); expect(cache?.user?.name).toBeDefined(); }); it("should work with JWT strategy", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", session: { cookieCache: { enabled: true, strategy: "jwt", }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", strategy: "jwt", }); expect(cache).not.toBeNull(); expect(cache?.user?.email).toEqual(testUser.email); expect(cache?.session?.token).toEqual(expect.any(String)); }); it("should work with base64-hmac strategy (legacy)", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", session: { cookieCache: { enabled: true, strategy: "base64-hmac", }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", strategy: "base64-hmac", }); expect(cache).not.toBeNull(); expect(cache?.user?.email).toEqual(testUser.email); expect(cache?.session?.token).toEqual(expect.any(String)); }); it("should return null for invalid JWT token", async () => { const { cookieSetter } = await getTestInstance({ secret: "better-auth.secret", session: { cookieCache: { enabled: true, strategy: "jwt", }, }, }); const headers = new Headers(); // Set an invalid JWT token manually headers.set("cookie", "better-auth.session_data=invalid.jwt.token"); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", strategy: "jwt", }); expect(cache).toBeNull(); }); it("should default to JWT strategy when not specified", async () => { const { client, testUser, cookieSetter } = await getTestInstance({ secret: "better-auth.secret", session: { cookieCache: { enabled: true, // No strategy specified, should default to "jwt" }, }, }); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess: cookieSetter(headers), }, ); const request = new Request("https://example.com/api/auth/session", { headers, }); const cache = await getCookieCache(request, { secret: "better-auth.secret", // No strategy specified, should default to "jwt" }); expect(cache).not.toBeNull(); expect(cache?.user?.email).toEqual(testUser.email); expect(cache?.session?.token).toEqual(expect.any(String)); }); }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/social.test.ts: -------------------------------------------------------------------------------- ```typescript import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { setupServer } from "msw/node"; import { http, HttpResponse } from "msw"; import { getTestInstance } from "./test-utils/test-instance"; import { DEFAULT_SECRET } from "./utils/constants"; import type { GoogleProfile } from "@better-auth/core/social-providers"; import { parseSetCookieHeader } from "./cookies"; import { refreshAccessToken } from "@better-auth/core/oauth2"; import { signJWT } from "./crypto"; import { OAuth2Server } from "oauth2-mock-server"; import { betterFetch } from "@better-fetch/fetch"; import Database from "better-sqlite3"; import { getMigrations } from "./db"; import { runWithEndpointContext } from "@better-auth/core/context"; import type { GenericEndpointContext } from "@better-auth/core"; let server = new OAuth2Server(); let port = 8005; const mswServer = setupServer(); let shouldUseUpdatedProfile = false; beforeAll(async () => { mswServer.listen({ onUnhandledRequest: "bypass" }); mswServer.use( http.post("https://oauth2.googleapis.com/token", async () => { const data: GoogleProfile = shouldUseUpdatedProfile ? { email: "[email protected]", email_verified: true, name: "Updated User", picture: "https://test.com/picture.png", exp: 1234567890, sub: "1234567890", iat: 1234567890, aud: "test", azp: "test", nbf: 1234567890, iss: "test", locale: "en", jti: "test", given_name: "Updated", family_name: "User", } : { email: "[email protected]", email_verified: true, name: "First Last", picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw", exp: 1234567890, sub: "1234567890", iat: 1234567890, aud: "test", azp: "test", nbf: 1234567890, iss: "test", locale: "en", jti: "test", given_name: "First", family_name: "Last", }; const testIdToken = await signJWT(data, DEFAULT_SECRET); return HttpResponse.json({ access_token: "test", refresh_token: "test", id_token: testIdToken, }); }), http.post(`http://localhost:${port}/token`, async () => { const data: GoogleProfile = { email: "[email protected]", email_verified: true, name: "First Last", picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw", exp: 1234567890, sub: "1234567890", iat: 1234567890, aud: "test", azp: "test", nbf: 1234567890, iss: "test", locale: "en", jti: "test", given_name: "First", family_name: "Last", }; const testIdToken = await signJWT(data, DEFAULT_SECRET); return HttpResponse.json({ access_token: "new-access-token", refresh_token: "new-refresh-token", id_token: testIdToken, token_type: "Bearer", expires_in: 3600, }); }), ); }); afterEach(() => { shouldUseUpdatedProfile = false; }); afterAll(() => mswServer.close()); describe("Social Providers", async (c) => { const { client, cookieSetter } = await getTestInstance( { user: { additionalFields: { firstName: { type: "string", }, lastName: { type: "string", }, isOAuth: { type: "boolean", }, }, }, socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, mapProfileToUser(profile) { return { firstName: profile.given_name, lastName: profile.family_name, isOAuth: true, }; }, }, apple: { clientId: "test", clientSecret: "test", }, }, }, { disableTestUser: true, }, ); beforeAll(async () => { await server.issuer.keys.generate("RS256"); server.issuer.on; await server.start(port, "localhost"); console.log("Issuer URL:", server.issuer.url); // -> http://localhost:${port} }); afterAll(async () => { await server.stop().catch(console.error); }); server.service.on("beforeRsponse", (tokenResponse, req) => { tokenResponse.body = { accessToken: "access-token", refreshToken: "refresher-token", }; tokenResponse.statusCode = 200; }); server.service.on("beforeUserinfo", (userInfoResponse, req) => { userInfoResponse.body = { email: "[email protected]", name: "OAuth2 Test", sub: "oauth2", picture: "https://test.com/picture.png", email_verified: true, }; userInfoResponse.statusCode = 200; }); server.service.on("beforeTokenSigning", (token, req) => { token.payload.email = "sso-user@localhost:8000.com"; token.payload.email_verified = true; token.payload.name = "Test User"; token.payload.picture = "https://test.com/picture.png"; }); const headers = new Headers(); async function simulateOAuthFlowRefresh( authUrl: string, headers: Headers, fetchImpl?: (...args: any) => any, ) { let location: string | null = null; await betterFetch(authUrl, { method: "GET", redirect: "manual", onError(context) { location = context.response.headers.get("location"); }, }); if (!location) throw new Error("No redirect location found"); const tokens = await refreshAccessToken({ refreshToken: "mock-refresh-token", options: { clientId: "test-client-id", clientKey: "test-client-key", clientSecret: "test-client-secret", }, tokenEndpoint: `http://localhost:${port}/token`, }); return tokens; } it("should be able to add social providers", async () => { const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", newUserCallbackURL: "/welcome", }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); }); it("should be able to sign in with social providers", async () => { const headers = new Headers(); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", newUserCallbackURL: "/welcome", fetchOptions: { onSuccess: cookieSetter(headers), }, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toBeDefined(); expect(location).toContain("/welcome"); const cookies = parseSetCookieHeader( context.response.headers.get("set-cookie") || "", ); expect(cookies.get("better-auth.session_token")?.value).toBeDefined(); }, }); }); it("Should use callback URL if the user is already registered", async () => { const headers = new Headers(); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", newUserCallbackURL: "/welcome", fetchOptions: { onSuccess: cookieSetter(headers), }, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toBeDefined(); expect(location).toContain("/callback"); const cookies = parseSetCookieHeader( context.response.headers.get("set-cookie") || "", ); expect(cookies.get("better-auth.session_token")?.value).toBeDefined(); }, }); }); it("should be able to map profile to user", async () => { const headers = new Headers(); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError: (c) => { //TODO: fix this cookieSetter(headers)(c as any); }, }); const session = await client.getSession({ fetchOptions: { headers, }, }); expect(session.data?.user).toMatchObject({ isOAuth: true, firstName: "First", lastName: "Last", }); }); it("should be protected from callback URL attacks", async () => { const signInRes = await client.signIn.social( { provider: "google", callbackURL: "https://evil.com/callback", }, { onSuccess(context) { const cookies = parseSetCookieHeader( context.response.headers.get("set-cookie") || "", ); headers.set( "cookie", `better-auth.state=${cookies.get("better-auth.state")?.value}`, ); }, }, ); expect(signInRes.error?.status).toBe(403); expect(signInRes.error?.message).toBe("Invalid callbackURL"); }); it("should refresh the access token", async () => { const headers = new Headers(); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", newUserCallbackURL: "/welcome", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toBeDefined(); expect(location).toContain("/callback"); const cookies = parseSetCookieHeader( context.response.headers.get("set-cookie") || "", ); cookieSetter(headers)(context as any); expect(cookies.get("better-auth.session_token")?.value).toBeDefined(); }, }); const accounts = await client.listAccounts({ fetchOptions: { headers }, }); await client.$fetch("/refresh-token", { body: { accountId: "test-id", providerId: "google", }, headers, method: "POST", onError(context) { cookieSetter(headers)(context as any); }, }); const authUrl = signInRes.data?.url; if (!authUrl) throw new Error("No auth url found"); const mockEndpoint = authUrl.replace( "https://accounts.google.com/o/oauth2/auth", `http://localhost:${port}/authorize`, ); const result = await simulateOAuthFlowRefresh(mockEndpoint, headers); const { accessToken, refreshToken } = result; expect({ accessToken, refreshToken }).toEqual({ accessToken: "new-access-token", refreshToken: "new-refresh-token", }); }); }); describe("Redirect URI", async () => { it("should infer redirect uri", async () => { const { client } = await getTestInstance({ basePath: "/custom/path", socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, }, }, }); await client.signIn.social( { provider: "google", callbackURL: "/callback", }, { onSuccess(context) { const redirectURI = context.data.url; expect(redirectURI).toContain( "http%3A%2F%2Flocalhost%3A3000%2Fcustom%2Fpath%2Fcallback%2Fgoogle", ); }, }, ); }); it("should respect custom redirect uri", async () => { const { client } = await getTestInstance({ socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, redirectURI: "https://test.com/callback", }, }, }); await client.signIn.social( { provider: "google", callbackURL: "/callback", }, { onSuccess(context) { const redirectURI = context.data.url; expect(redirectURI).toContain( "redirect_uri=https%3A%2F%2Ftest.com%2Fcallback", ); }, }, ); }); }); describe("Disable implicit signup", async () => { it("Should not create user when implicit sign up is disabled", async () => { const { client, cookieSetter } = await getTestInstance({ socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, disableImplicitSignUp: true, }, }, }); const headers = new Headers(); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", newUserCallbackURL: "/welcome", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toBeDefined(); expect(location).toContain( "http://localhost:3000/api/auth/error?error=signup_disabled", ); }, }); }); it("Should create user when implicit sign up is disabled but it is requested", async () => { const { client, cookieSetter } = await getTestInstance({ socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, disableImplicitSignUp: true, }, }, }); const headers = new Headers(); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", newUserCallbackURL: "/welcome", requestSignUp: true, fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toBeDefined(); expect(location).toContain("/welcome"); const cookies = parseSetCookieHeader( context.response.headers.get("set-cookie") || "", ); expect(cookies.get("better-auth.session_token")?.value).toBeDefined(); }, }); }); }); describe("Disable signup", async () => { it("Should not create user when sign up is disabled", async () => { const headers = new Headers(); const { client, cookieSetter } = await getTestInstance({ socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, disableSignUp: true, }, }, }); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", newUserCallbackURL: "/welcome", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toBeDefined(); expect(location).toContain( "http://localhost:3000/api/auth/error?error=signup_disabled", ); }, }); }); }); describe("signin", async () => { const database = new Database(":memory:"); beforeAll(async () => { const migrations = await getMigrations({ database, }); await migrations.runMigrations(); }); it("should allow user info override during sign in", async () => { let state = ""; const headers = new Headers(); const { client, cookieSetter } = await getTestInstance({ database, socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, }, }, }); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError: (c) => { cookieSetter(headers)(c as any); }, }); const session = await client.getSession({ fetchOptions: { headers, }, }); expect(session.data?.user).toMatchObject({ name: "First Last", }); }); it("should allow user info override during sign in", async () => { shouldUseUpdatedProfile = true; const headers = new Headers(); let state = ""; const { client, cookieSetter } = await getTestInstance( { database, socialProviders: { google: { clientId: "test", clientSecret: "test", enabled: true, overrideUserInfoOnSignIn: true, }, }, }, { disableTestUser: true, }, ); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, headers, method: "GET", onError: (c) => { cookieSetter(headers)(c as any); }, }); const session = await client.getSession({ fetchOptions: { headers, }, }); expect(session.data?.user).toMatchObject({ name: "Updated User", }); }); }); describe("updateAccountOnSignIn", async () => { const { client, cookieSetter, auth } = await getTestInstance({ account: { updateAccountOnSignIn: false, }, }); const ctx = await auth.$context; it("should not update account on sign in", async () => { const headers = new Headers(); const signInRes = await client.signIn.social({ provider: "google", callbackURL: "/callback", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); const state = new URL(signInRes.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state, code: "test", }, method: "GET", headers, onError(context) { cookieSetter(headers)(context as any); }, }); const session = await client.getSession({ fetchOptions: { headers, }, }); const userAccounts = await ctx.internalAdapter.findAccounts( session.data?.user.id!, ); await runWithEndpointContext( { context: ctx, } as GenericEndpointContext, () => ctx.internalAdapter.updateAccount(userAccounts[0]!.id, { accessToken: "new-access-token", }), ); //re-sign in const signInRes2 = await client.signIn.social({ provider: "google", callbackURL: "/callback", fetchOptions: { onSuccess: cookieSetter(headers), }, }); expect(signInRes2.data).toMatchObject({ url: expect.stringContaining("google.com"), redirect: true, }); const state2 = new URL(signInRes2.data!.url!).searchParams.get("state") || ""; await client.$fetch("/callback/google", { query: { state: state2, code: "test", }, headers, method: "GET", onError(context) { cookieSetter(headers)(context as any); }, }); const session2 = await client.getSession({ fetchOptions: { headers, }, }); const userAccounts2 = await ctx.internalAdapter.findAccounts( session2.data?.user.id!, ); expect(userAccounts2[0]!.accessToken).toBe("new-access-token"); }); }); ``` -------------------------------------------------------------------------------- /packages/cli/test/get-config.test.ts: -------------------------------------------------------------------------------- ```typescript import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { test } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import { getConfig } from "../src/utils/get-config"; interface TmpDirFixture { tmpdir: string; } async function createTempDir() { const tmpdir = path.join(process.cwd(), "test", "getConfig_test-"); return await fs.mkdtemp(tmpdir); } export const tmpdirTest = test.extend<TmpDirFixture>({ tmpdir: async ({}, use) => { const directory = await createTempDir(); await use(directory); await fs.rm(directory, { recursive: true }); }, }); let tmpDir = "."; describe("getConfig", async () => { beforeEach(async () => { const tmp = path.join(process.cwd(), "getConfig_test-"); tmpDir = await fs.mkdtemp(tmp); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true }); }); it("should resolve resolver type alias", async () => { const authPath = path.join(tmpDir, "server", "auth"); const dbPath = path.join(tmpDir, "server", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); //create dummy tsconfig.json await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { /* Path Aliases */ "baseUrl": ".", "paths": { "@server/*": ["./server/*"] } } }`, ); //create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "@server/db/db"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); //create dummy db.ts await fs.writeFile( path.join(dbPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should resolve direct alias", async () => { const authPath = path.join(tmpDir, "server", "auth"); const dbPath = path.join(tmpDir, "server", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); //create dummy tsconfig.json await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { /* Path Aliases */ "baseUrl": ".", "paths": { "prismaDbClient": ["./server/db/db"] } } }`, ); //create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "prismaDbClient"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); //create dummy db.ts await fs.writeFile( path.join(dbPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should resolve resolver type alias with relative path", async () => { const authPath = path.join(tmpDir, "test", "server", "auth"); const dbPath = path.join(tmpDir, "test", "server", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); //create dummy tsconfig.json await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { /* Path Aliases */ "baseUrl": "./test", "paths": { "@server/*": ["./server/*"] } } }`, ); //create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "@server/db/db"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); //create dummy db.ts await fs.writeFile( path.join(dbPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const config = await getConfig({ cwd: tmpDir, configPath: "test/server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should resolve direct alias with relative path", async () => { const authPath = path.join(tmpDir, "test", "server", "auth"); const dbPath = path.join(tmpDir, "test", "server", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); //create dummy tsconfig.json await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { /* Path Aliases */ "baseUrl": "./test", "paths": { "prismaDbClient": ["./server/db/db"] } } }`, ); //create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "prismaDbClient"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); //create dummy db.ts await fs.writeFile( path.join(dbPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const config = await getConfig({ cwd: tmpDir, configPath: "test/server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should resolve with relative import", async () => { const authPath = path.join(tmpDir, "test", "server", "auth"); const dbPath = path.join(tmpDir, "test", "server", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); //create dummy tsconfig.json await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { /* Path Aliases */ "baseUrl": "./test", "paths": { "prismaDbClient": ["./server/db/db"] } } }`, ); //create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "../db/db"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); //create dummy db.ts await fs.writeFile( path.join(dbPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const config = await getConfig({ cwd: tmpDir, configPath: "test/server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should error with invalid alias", async () => { const authPath = path.join(tmpDir, "server", "auth"); const dbPath = path.join(tmpDir, "server", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); //create dummy tsconfig.json await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { /* Path Aliases */ "baseUrl": ".", "paths": { "@server/*": ["./PathIsInvalid/*"] } } }`, ); //create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "@server/db/db"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); //create dummy db.ts await fs.writeFile( path.join(dbPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); await expect(() => getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts" }), ).rejects.toThrowError(); expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining("Couldn't read your auth config."), expect.objectContaining({ code: "MODULE_NOT_FOUND", }), ); }); it("should resolve js config", async () => { const authPath = path.join(tmpDir, "server", "auth"); const dbPath = path.join(tmpDir, "server", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); //create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.js"), `import { betterAuth } from "better-auth"; export const auth = betterAuth({ emailAndPassword: { enabled: true, } })`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.js", }); expect(config).toMatchObject({ emailAndPassword: { enabled: true }, }); }); it("should resolve path aliases from referenced tsconfig files", async () => { const authPath = path.join(tmpDir, "apps", "web", "server", "auth"); const dbPath = path.join(tmpDir, "packages", "shared", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(dbPath, { recursive: true }); // Create root tsconfig.json with references await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "references": [ { "path": "./apps/web" }, { "path": "./packages/shared" } ] }`, ); // Create web app tsconfig.json with aliases await fs.writeFile( path.join(tmpDir, "apps", "web", "tsconfig.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@web/*": ["./server/*"] } } }`, ); // Create shared package tsconfig.json with aliases await fs.writeFile( path.join(tmpDir, "packages", "shared", "tsconfig.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@shared/*": ["./db/*"] } } }`, ); // Create dummy auth.ts using both aliases await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "@shared/db"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); // Create dummy db.ts await fs.writeFile( path.join(dbPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const config = await getConfig({ cwd: tmpDir, configPath: "apps/web/server/auth/auth.ts", }); expect(config).toMatchObject({ emailAndPassword: { enabled: true }, database: expect.objectContaining({ // This proves the @shared/db alias was resolved correctly }), }); }); it("should handle missing referenced tsconfig files gracefully", async () => { const authPath = path.join(tmpDir, "server", "auth"); await fs.mkdir(authPath, { recursive: true }); // Create root tsconfig.json with reference to non-existent file await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@server/*": ["./server/*"] } }, "references": [ { "path": "./non-existent" } ] }`, ); // Create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; export const auth = betterAuth({ emailAndPassword: { enabled: true, } })`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should handle circular references in tsconfig files", async () => { const authPath = path.join(tmpDir, "server", "auth"); const appPath = path.join(tmpDir, "app"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(appPath, { recursive: true }); // Create root tsconfig.json that references app tsconfig await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@root/*": ["./server/*"] } }, "references": [ { "path": "./app" } ] }`, ); // Create app tsconfig.json that references back to root await fs.writeFile( path.join(tmpDir, "app", "tsconfig.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@app/*": ["./src/*"] } }, "references": [ { "path": ".." } ] }`, ); // Create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; export const auth = betterAuth({ emailAndPassword: { enabled: true, } })`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should resolve direct tsconfig file references", async () => { const authPath = path.join(tmpDir, "server", "auth"); const sharedPath = path.join(tmpDir, "shared", "db"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(sharedPath, { recursive: true }); // Create root tsconfig.json with direct file references await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.shared.json" } ] }`, ); // Create tsconfig.app.json with app-specific aliases await fs.writeFile( path.join(tmpDir, "tsconfig.app.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@app/*": ["./server/*"] } } }`, ); // Create tsconfig.shared.json with shared aliases await fs.writeFile( path.join(tmpDir, "tsconfig.shared.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@shared/*": ["./shared/*"] } } }`, ); // Create dummy auth.ts using both aliases await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; import {prismaAdapter} from "better-auth/adapters/prisma"; import {db} from "@shared/db/db"; export const auth = betterAuth({ database: prismaAdapter(db, { provider: 'sqlite' }), emailAndPassword: { enabled: true, } })`, ); // Create dummy db.ts await fs.writeFile( path.join(sharedPath, "db.ts"), `class PrismaClient { constructor() {} } export const db = new PrismaClient()`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should handle mixed directory and file references", async () => { const authPath = path.join(tmpDir, "apps", "web", "server", "auth"); const utilsPath = path.join(tmpDir, "packages", "utils"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(utilsPath, { recursive: true }); // Create root tsconfig.json with mixed references await fs.writeFile( path.join(tmpDir, "tsconfig.json"), `{ "references": [ { "path": "./apps/web" }, { "path": "./tsconfig.utils.json" } ] }`, ); // Create web app directory-based tsconfig await fs.writeFile( path.join(tmpDir, "apps", "web", "tsconfig.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@web/*": ["./server/*"] } } }`, ); // Create utils file-based tsconfig await fs.writeFile( path.join(tmpDir, "tsconfig.utils.json"), `{ "compilerOptions": { "baseUrl": ".", "paths": { "@utils/*": ["./packages/utils/*"] } } }`, ); // Create dummy auth.ts await fs.writeFile( path.join(authPath, "auth.ts"), `import {betterAuth} from "better-auth"; export const auth = betterAuth({ emailAndPassword: { enabled: true, } })`, ); // Create dummy utils file await fs.writeFile( path.join(utilsPath, "index.ts"), `export const utils = {}`, ); const config = await getConfig({ cwd: tmpDir, configPath: "apps/web/server/auth/auth.ts", }); expect(config).not.toBe(null); }); it("should resolve SvelteKit $lib/server imports correctly", async () => { const authPath = path.join(tmpDir, "src"); const libServerPath = path.join(tmpDir, "src", "lib", "server"); await fs.mkdir(authPath, { recursive: true }); await fs.mkdir(libServerPath, { recursive: true }); await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-sveltekit", devDependencies: { "@sveltejs/kit": "^2.0.0", }, }), ); await fs.writeFile( path.join(libServerPath, "database.ts"), `export const db = { // Mock database client connect: () => console.log('Connected to database') };`, ); await fs.writeFile( path.join(authPath, "auth.ts"), `import { betterAuth } from "better-auth"; import { db } from "$lib/server/database"; export const auth = betterAuth({ emailAndPassword: { enabled: true, } })`, ); const config = await getConfig({ cwd: tmpDir, configPath: "src/auth.ts", }); expect(config).not.toBe(null); expect(config).toMatchObject({ emailAndPassword: { enabled: true }, }); }); it("should resolve export default auth", async () => { const authPath = path.join(tmpDir, "server", "auth"); await fs.mkdir(authPath, { recursive: true }); await fs.writeFile( path.join(authPath, "auth.ts"), `import { betterAuth } from "better-auth"; const auth = betterAuth({ emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: "test-id", clientSecret: "test-secret" } } }); export default auth;`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); expect(config).toMatchObject({ emailAndPassword: { enabled: true }, socialProviders: { github: { clientId: "test-id", clientSecret: "test-secret", }, }, }); }); it("should resolve export default auth with named export", async () => { const authPath = path.join(tmpDir, "server", "auth"); await fs.mkdir(authPath, { recursive: true }); await fs.writeFile( path.join(authPath, "auth.ts"), `import { betterAuth } from "better-auth"; const auth = betterAuth({ emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: "test-id", clientSecret: "test-secret" } } }); export { auth }; export default auth;`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); expect(config).toMatchObject({ emailAndPassword: { enabled: true }, socialProviders: { github: { clientId: "test-id", clientSecret: "test-secret", }, }, }); }); it("should resolve export default with inline betterAuth call", async () => { const authPath = path.join(tmpDir, "server", "auth"); await fs.mkdir(authPath, { recursive: true }); await fs.writeFile( path.join(authPath, "auth.ts"), `import { betterAuth } from "better-auth"; export default betterAuth({ emailAndPassword: { enabled: true, }, trustedOrigins: ["http://localhost:3000"] });`, ); const config = await getConfig({ cwd: tmpDir, configPath: "server/auth/auth.ts", }); expect(config).not.toBe(null); expect(config).toMatchObject({ emailAndPassword: { enabled: true }, trustedOrigins: ["http://localhost:3000"], }); }); }); ```