This is page 14 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; 2 | import parseJSON from "../../client/parser"; 3 | 4 | export const apiKeySchema = ({ 5 | timeWindow, 6 | rateLimitMax, 7 | }: { 8 | timeWindow: number; 9 | rateLimitMax: number; 10 | }) => 11 | ({ 12 | apikey: { 13 | fields: { 14 | /** 15 | * The name of the key. 16 | */ 17 | name: { 18 | type: "string", 19 | required: false, 20 | input: false, 21 | }, 22 | /** 23 | * Shows the first few characters of the API key 24 | * This allows you to show those few characters in the UI to make it easier for users to identify the API key. 25 | */ 26 | start: { 27 | type: "string", 28 | required: false, 29 | input: false, 30 | }, 31 | /** 32 | * The prefix of the key. 33 | */ 34 | prefix: { 35 | type: "string", 36 | required: false, 37 | input: false, 38 | }, 39 | /** 40 | * The hashed key value. 41 | */ 42 | key: { 43 | type: "string", 44 | required: true, 45 | input: false, 46 | }, 47 | /** 48 | * The user id of the user who created the key. 49 | */ 50 | userId: { 51 | type: "string", 52 | references: { model: "user", field: "id", onDelete: "cascade" }, 53 | required: true, 54 | input: false, 55 | }, 56 | /** 57 | * The interval to refill the key in milliseconds. 58 | */ 59 | refillInterval: { 60 | type: "number", 61 | required: false, 62 | input: false, 63 | }, 64 | /** 65 | * The amount to refill the remaining count of the key. 66 | */ 67 | refillAmount: { 68 | type: "number", 69 | required: false, 70 | input: false, 71 | }, 72 | /** 73 | * The date and time when the key was last refilled. 74 | */ 75 | lastRefillAt: { 76 | type: "date", 77 | required: false, 78 | input: false, 79 | }, 80 | /** 81 | * Whether the key is enabled. 82 | */ 83 | enabled: { 84 | type: "boolean", 85 | required: false, 86 | input: false, 87 | defaultValue: true, 88 | }, 89 | /** 90 | * Whether the key has rate limiting enabled. 91 | */ 92 | rateLimitEnabled: { 93 | type: "boolean", 94 | required: false, 95 | input: false, 96 | defaultValue: true, 97 | }, 98 | /** 99 | * The time window in milliseconds for the rate limit. 100 | */ 101 | rateLimitTimeWindow: { 102 | type: "number", 103 | required: false, 104 | input: false, 105 | defaultValue: timeWindow, 106 | }, 107 | /** 108 | * The maximum number of requests allowed within the `rateLimitTimeWindow`. 109 | */ 110 | rateLimitMax: { 111 | type: "number", 112 | required: false, 113 | input: false, 114 | defaultValue: rateLimitMax, 115 | }, 116 | /** 117 | * The number of requests made within the rate limit time window 118 | */ 119 | requestCount: { 120 | type: "number", 121 | required: false, 122 | input: false, 123 | defaultValue: 0, 124 | }, 125 | /** 126 | * The remaining number of requests before the key is revoked. 127 | * 128 | * If this is null, then the key is not revoked. 129 | * 130 | * If `refillInterval` & `refillAmount` are provided, than this will refill accordingly. 131 | */ 132 | remaining: { 133 | type: "number", 134 | required: false, 135 | input: false, 136 | }, 137 | /** 138 | * The date and time of the last request made to the key. 139 | */ 140 | lastRequest: { 141 | type: "date", 142 | required: false, 143 | input: false, 144 | }, 145 | /** 146 | * The date and time when the key will expire. 147 | */ 148 | expiresAt: { 149 | type: "date", 150 | required: false, 151 | input: false, 152 | }, 153 | /** 154 | * The date and time when the key was created. 155 | */ 156 | createdAt: { 157 | type: "date", 158 | required: true, 159 | input: false, 160 | }, 161 | /** 162 | * The date and time when the key was last updated. 163 | */ 164 | updatedAt: { 165 | type: "date", 166 | required: true, 167 | input: false, 168 | }, 169 | /** 170 | * The permissions of the key. 171 | */ 172 | permissions: { 173 | type: "string", 174 | required: false, 175 | input: false, 176 | }, 177 | /** 178 | * Any additional metadata you want to store with the key. 179 | */ 180 | metadata: { 181 | type: "string", 182 | required: false, 183 | input: true, 184 | transform: { 185 | input(value) { 186 | return JSON.stringify(value); 187 | }, 188 | output(value) { 189 | if (!value) return null; 190 | return parseJSON<any>(value as string); 191 | }, 192 | }, 193 | }, 194 | }, 195 | }, 196 | }) satisfies BetterAuthPluginDBSchema; 197 | ``` -------------------------------------------------------------------------------- /docs/app/changelogs/_components/stat-field.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useEffect, useId, useRef } from "react"; 4 | import clsx from "clsx"; 5 | import { animate, Segment } from "motion/react"; 6 | 7 | type Star = [x: number, y: number, dim?: boolean, blur?: boolean]; 8 | 9 | const stars: Array<Star> = [ 10 | [4, 4, true, true], 11 | [4, 44, true], 12 | [36, 22], 13 | [50, 146, true, true], 14 | [64, 43, true, true], 15 | [76, 30, true], 16 | [101, 116], 17 | [140, 36, true], 18 | [149, 134], 19 | [162, 74, true], 20 | [171, 96, true, true], 21 | [210, 56, true, true], 22 | [235, 90], 23 | [275, 82, true, true], 24 | [306, 6], 25 | [307, 64, true, true], 26 | [380, 68, true], 27 | [380, 108, true, true], 28 | [391, 148, true, true], 29 | [405, 18, true], 30 | [412, 86, true, true], 31 | [426, 210, true, true], 32 | [427, 56, true, true], 33 | [538, 138], 34 | [563, 88, true, true], 35 | [611, 154, true, true], 36 | [637, 150], 37 | [651, 146, true], 38 | [682, 70, true, true], 39 | [683, 128], 40 | [781, 82, true, true], 41 | [785, 158, true], 42 | [832, 146, true, true], 43 | [852, 89], 44 | ]; 45 | 46 | const constellations: Array<Array<Star>> = [ 47 | [ 48 | [247, 103], 49 | [261, 86], 50 | [307, 104], 51 | [357, 36], 52 | ], 53 | [ 54 | [586, 120], 55 | [516, 100], 56 | [491, 62], 57 | [440, 107], 58 | [477, 180], 59 | [516, 100], 60 | ], 61 | [ 62 | [733, 100], 63 | [803, 120], 64 | [879, 113], 65 | [823, 164], 66 | [803, 120], 67 | ], 68 | ]; 69 | 70 | function Star({ 71 | blurId, 72 | point: [cx, cy, dim, blur], 73 | }: { 74 | blurId: string; 75 | point: Star; 76 | }) { 77 | let groupRef = useRef<React.ElementRef<"g">>(null); 78 | let ref = useRef<React.ElementRef<"circle">>(null); 79 | 80 | useEffect(() => { 81 | if (!groupRef.current || !ref.current) { 82 | return; 83 | } 84 | 85 | let delay = Math.random() * 2; 86 | 87 | let animations = [ 88 | animate(groupRef.current, { opacity: 1 }, { duration: 4, delay }), 89 | animate( 90 | ref.current, 91 | { 92 | opacity: dim ? [0.2, 0.5] : [1, 0.6], 93 | scale: dim ? [1, 1.2] : [1.2, 1], 94 | }, 95 | { 96 | duration: 10, 97 | delay, 98 | }, 99 | ), 100 | ]; 101 | 102 | return () => { 103 | for (let animation of animations) { 104 | animation.cancel(); 105 | } 106 | }; 107 | }, [dim]); 108 | 109 | return ( 110 | <g ref={groupRef} className="opacity-0"> 111 | <circle 112 | ref={ref} 113 | cx={cx} 114 | cy={cy} 115 | r={1} 116 | style={{ 117 | transformOrigin: `${cx / 16}rem ${cy / 16}rem`, 118 | opacity: dim ? 0.2 : 1, 119 | transform: `scale(${dim ? 1 : 1.2})`, 120 | }} 121 | filter={blur ? `url(#${blurId})` : undefined} 122 | /> 123 | </g> 124 | ); 125 | } 126 | 127 | function Constellation({ 128 | points, 129 | blurId, 130 | }: { 131 | points: Array<Star>; 132 | blurId: string; 133 | }) { 134 | let ref = useRef<React.ElementRef<"path">>(null); 135 | let uniquePoints = points.filter( 136 | (point, pointIndex) => 137 | points.findIndex((p) => String(p) === String(point)) === pointIndex, 138 | ); 139 | let isFilled = uniquePoints.length !== points.length; 140 | 141 | useEffect(() => { 142 | if (!ref.current) { 143 | return; 144 | } 145 | 146 | let sequence: Array<Segment> = [ 147 | [ 148 | ref.current, 149 | { strokeDashoffset: 0, visibility: "visible" }, 150 | { duration: 5, delay: Math.random() * 3 + 2 }, 151 | ], 152 | ]; 153 | 154 | if (isFilled) { 155 | sequence.push([ 156 | ref.current, 157 | { fill: "rgb(255 255 255 / 0.02)" }, 158 | { duration: 1 }, 159 | ]); 160 | } 161 | 162 | let animation = animate(sequence); 163 | 164 | return () => { 165 | animation.cancel(); 166 | }; 167 | }, [isFilled]); 168 | 169 | return ( 170 | <> 171 | <path 172 | ref={ref} 173 | stroke="white" 174 | strokeOpacity="0.2" 175 | strokeDasharray={1} 176 | strokeDashoffset={1} 177 | pathLength={1} 178 | fill="transparent" 179 | d={`M ${points.join("L")}`} 180 | className="invisible" 181 | /> 182 | {uniquePoints.map((point, pointIndex) => ( 183 | <Star key={pointIndex} point={point} blurId={blurId} /> 184 | ))} 185 | </> 186 | ); 187 | } 188 | 189 | export function StarField({ className }: { className?: string }) { 190 | let blurId = useId(); 191 | 192 | return ( 193 | <svg 194 | viewBox="0 0 881 211" 195 | fill="white" 196 | aria-hidden="true" 197 | className={clsx( 198 | "pointer-events-none absolute w-[55.0625rem] origin-top-right rotate-[30deg] overflow-visible opacity-70", 199 | className, 200 | )} 201 | > 202 | <defs> 203 | <filter id={blurId}> 204 | <feGaussianBlur in="SourceGraphic" stdDeviation=".5" /> 205 | </filter> 206 | </defs> 207 | {constellations.map((points, constellationIndex) => ( 208 | <Constellation 209 | key={constellationIndex} 210 | points={points} 211 | blurId={blurId} 212 | /> 213 | ))} 214 | {stars.map((point, pointIndex) => ( 215 | <Star key={pointIndex} point={point} blurId={blurId} /> 216 | ))} 217 | </svg> 218 | ); 219 | } 220 | ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/stat-field.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useEffect, useId, useRef } from "react"; 4 | import clsx from "clsx"; 5 | import { animate, Segment } from "motion/react"; 6 | 7 | type Star = [x: number, y: number, dim?: boolean, blur?: boolean]; 8 | 9 | const stars: Array<Star> = [ 10 | [4, 4, true, true], 11 | [4, 44, true], 12 | [36, 22], 13 | [50, 146, true, true], 14 | [64, 43, true, true], 15 | [76, 30, true], 16 | [101, 116], 17 | [140, 36, true], 18 | [149, 134], 19 | [162, 74, true], 20 | [171, 96, true, true], 21 | [210, 56, true, true], 22 | [235, 90], 23 | [275, 82, true, true], 24 | [306, 6], 25 | [307, 64, true, true], 26 | [380, 68, true], 27 | [380, 108, true, true], 28 | [391, 148, true, true], 29 | [405, 18, true], 30 | [412, 86, true, true], 31 | [426, 210, true, true], 32 | [427, 56, true, true], 33 | [538, 138], 34 | [563, 88, true, true], 35 | [611, 154, true, true], 36 | [637, 150], 37 | [651, 146, true], 38 | [682, 70, true, true], 39 | [683, 128], 40 | [781, 82, true, true], 41 | [785, 158, true], 42 | [832, 146, true, true], 43 | [852, 89], 44 | ]; 45 | 46 | const constellations: Array<Array<Star>> = [ 47 | [ 48 | [247, 103], 49 | [261, 86], 50 | [307, 104], 51 | [357, 36], 52 | ], 53 | [ 54 | [586, 120], 55 | [516, 100], 56 | [491, 62], 57 | [440, 107], 58 | [477, 180], 59 | [516, 100], 60 | ], 61 | [ 62 | [733, 100], 63 | [803, 120], 64 | [879, 113], 65 | [823, 164], 66 | [803, 120], 67 | ], 68 | ]; 69 | 70 | function Star({ 71 | blurId, 72 | point: [cx, cy, dim, blur], 73 | }: { 74 | blurId: string; 75 | point: Star; 76 | }) { 77 | let groupRef = useRef<React.ElementRef<"g">>(null); 78 | let ref = useRef<React.ElementRef<"circle">>(null); 79 | 80 | useEffect(() => { 81 | if (!groupRef.current || !ref.current) { 82 | return; 83 | } 84 | 85 | let delay = Math.random() * 2; 86 | 87 | let animations = [ 88 | animate(groupRef.current, { opacity: 1 }, { duration: 4, delay }), 89 | animate( 90 | ref.current, 91 | { 92 | opacity: dim ? [0.2, 0.5] : [1, 0.6], 93 | scale: dim ? [1, 1.2] : [1.2, 1], 94 | }, 95 | { 96 | duration: 10, 97 | delay, 98 | }, 99 | ), 100 | ]; 101 | 102 | return () => { 103 | for (let animation of animations) { 104 | animation.cancel(); 105 | } 106 | }; 107 | }, [dim]); 108 | 109 | return ( 110 | <g ref={groupRef} className="opacity-0"> 111 | <circle 112 | ref={ref} 113 | cx={cx} 114 | cy={cy} 115 | r={1} 116 | style={{ 117 | transformOrigin: `${cx / 16}rem ${cy / 16}rem`, 118 | opacity: dim ? 0.2 : 1, 119 | transform: `scale(${dim ? 1 : 1.2})`, 120 | }} 121 | filter={blur ? `url(#${blurId})` : undefined} 122 | /> 123 | </g> 124 | ); 125 | } 126 | 127 | function Constellation({ 128 | points, 129 | blurId, 130 | }: { 131 | points: Array<Star>; 132 | blurId: string; 133 | }) { 134 | let ref = useRef<React.ElementRef<"path">>(null); 135 | let uniquePoints = points.filter( 136 | (point, pointIndex) => 137 | points.findIndex((p) => String(p) === String(point)) === pointIndex, 138 | ); 139 | let isFilled = uniquePoints.length !== points.length; 140 | 141 | useEffect(() => { 142 | if (!ref.current) { 143 | return; 144 | } 145 | 146 | let sequence: Array<Segment> = [ 147 | [ 148 | ref.current, 149 | { strokeDashoffset: 0, visibility: "visible" }, 150 | { duration: 5, delay: Math.random() * 3 + 2 }, 151 | ], 152 | ]; 153 | 154 | if (isFilled) { 155 | sequence.push([ 156 | ref.current, 157 | { fill: "rgb(255 255 255 / 0.02)" }, 158 | { duration: 1 }, 159 | ]); 160 | } 161 | 162 | let animation = animate(sequence); 163 | 164 | return () => { 165 | animation.cancel(); 166 | }; 167 | }, [isFilled]); 168 | 169 | return ( 170 | <> 171 | <path 172 | ref={ref} 173 | stroke="white" 174 | strokeOpacity="0.2" 175 | strokeDasharray={1} 176 | strokeDashoffset={1} 177 | pathLength={1} 178 | fill="transparent" 179 | d={`M ${points.join("L")}`} 180 | className="invisible" 181 | /> 182 | {uniquePoints.map((point, pointIndex) => ( 183 | <Star key={pointIndex} point={point} blurId={blurId} /> 184 | ))} 185 | </> 186 | ); 187 | } 188 | 189 | export function StarField({ className }: { className?: string }) { 190 | let blurId = useId(); 191 | 192 | return ( 193 | <svg 194 | viewBox="0 0 881 211" 195 | fill="white" 196 | aria-hidden="true" 197 | className={clsx( 198 | "pointer-events-none absolute w-[55.0625rem] max-w-[100vw] origin-top-right rotate-[30deg] overflow-visible opacity-70", 199 | className, 200 | )} 201 | > 202 | <defs> 203 | <filter id={blurId}> 204 | <feGaussianBlur in="SourceGraphic" stdDeviation=".5" /> 205 | </filter> 206 | </defs> 207 | {constellations.map((points, constellationIndex) => ( 208 | <Constellation 209 | key={constellationIndex} 210 | points={points} 211 | blurId={blurId} 212 | /> 213 | ))} 214 | {stars.map((point, pointIndex) => ( 215 | <Star key={pointIndex} point={point} blurId={blurId} /> 216 | ))} 217 | </svg> 218 | ); 219 | } 220 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/parser.ts: -------------------------------------------------------------------------------- ```typescript 1 | const PROTO_POLLUTION_PATTERNS = { 2 | proto: 3 | /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/, 4 | constructor: 5 | /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/, 6 | protoShort: /"__proto__"\s*:/, 7 | constructorShort: /"constructor"\s*:/, 8 | } as const; 9 | 10 | const JSON_SIGNATURE = 11 | /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/; 12 | 13 | const SPECIAL_VALUES = { 14 | true: true, 15 | false: false, 16 | null: null, 17 | undefined: undefined, 18 | nan: Number.NaN, 19 | infinity: Number.POSITIVE_INFINITY, 20 | "-infinity": Number.NEGATIVE_INFINITY, 21 | } as const; 22 | 23 | const ISO_DATE_REGEX = 24 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?(?:Z|([+-])(\d{2}):(\d{2}))$/; 25 | 26 | type ParseOptions = { 27 | /** Throw errors instead of returning the original value */ 28 | strict?: boolean; 29 | /** Log warnings when suspicious patterns are detected */ 30 | warnings?: boolean; 31 | /** Custom reviver function */ 32 | reviver?: (key: string, value: any) => any; 33 | /** Automatically convert ISO date strings to Date objects */ 34 | parseDates?: boolean; 35 | }; 36 | 37 | function isValidDate(date: Date): boolean { 38 | return date instanceof Date && !isNaN(date.getTime()); 39 | } 40 | 41 | function parseISODate(value: string): Date | null { 42 | const match = ISO_DATE_REGEX.exec(value); 43 | if (!match) return null; 44 | 45 | const [ 46 | , 47 | year, 48 | month, 49 | day, 50 | hour, 51 | minute, 52 | second, 53 | ms, 54 | offsetSign, 55 | offsetHour, 56 | offsetMinute, 57 | ] = match; 58 | 59 | let date = new Date( 60 | Date.UTC( 61 | parseInt(year!, 10), 62 | parseInt(month!, 10) - 1, 63 | parseInt(day!, 10), 64 | parseInt(hour!, 10), 65 | parseInt(minute!, 10), 66 | parseInt(second!, 10), 67 | ms ? parseInt(ms.padEnd(3, "0"), 10) : 0, 68 | ), 69 | ); 70 | 71 | if (offsetSign) { 72 | const offset = 73 | (parseInt(offsetHour!, 10) * 60 + parseInt(offsetMinute!, 10)) * 74 | (offsetSign === "+" ? -1 : 1); 75 | date.setUTCMinutes(date.getUTCMinutes() + offset); 76 | } 77 | 78 | return isValidDate(date) ? date : null; 79 | } 80 | 81 | function betterJSONParse<T = unknown>( 82 | value: unknown, 83 | options: ParseOptions = {}, 84 | ): T { 85 | const { 86 | strict = false, 87 | warnings = false, 88 | reviver, 89 | parseDates = true, 90 | } = options; 91 | 92 | if (typeof value !== "string") { 93 | return value as T; 94 | } 95 | 96 | const trimmed = value.trim(); 97 | 98 | if ( 99 | trimmed.length > 0 && 100 | trimmed[0] === '"' && 101 | trimmed.endsWith('"') && 102 | !trimmed.slice(1, -1).includes('"') 103 | ) { 104 | return trimmed.slice(1, -1) as T; 105 | } 106 | 107 | const lowerValue = trimmed.toLowerCase(); 108 | if (lowerValue.length <= 9 && lowerValue in SPECIAL_VALUES) { 109 | return SPECIAL_VALUES[lowerValue as keyof typeof SPECIAL_VALUES] as T; 110 | } 111 | 112 | if (!JSON_SIGNATURE.test(trimmed)) { 113 | if (strict) { 114 | throw new SyntaxError("[better-json] Invalid JSON"); 115 | } 116 | return value as T; 117 | } 118 | 119 | const hasProtoPattern = Object.entries(PROTO_POLLUTION_PATTERNS).some( 120 | ([key, pattern]) => { 121 | const matches = pattern.test(trimmed); 122 | if (matches && warnings) { 123 | console.warn( 124 | `[better-json] Detected potential prototype pollution attempt using ${key} pattern`, 125 | ); 126 | } 127 | return matches; 128 | }, 129 | ); 130 | 131 | if (hasProtoPattern && strict) { 132 | throw new Error( 133 | "[better-json] Potential prototype pollution attempt detected", 134 | ); 135 | } 136 | 137 | try { 138 | const secureReviver = (key: string, value: any) => { 139 | if ( 140 | key === "__proto__" || 141 | (key === "constructor" && 142 | value && 143 | typeof value === "object" && 144 | "prototype" in value) 145 | ) { 146 | if (warnings) { 147 | console.warn( 148 | `[better-json] Dropping "${key}" key to prevent prototype pollution`, 149 | ); 150 | } 151 | return undefined; 152 | } 153 | 154 | if (parseDates && typeof value === "string") { 155 | const date = parseISODate(value); 156 | if (date) { 157 | return date; 158 | } 159 | } 160 | 161 | return reviver ? reviver(key, value) : value; 162 | }; 163 | 164 | return JSON.parse(trimmed, secureReviver); 165 | } catch (error) { 166 | if (strict) { 167 | throw error; 168 | } 169 | return value as T; 170 | } 171 | } 172 | 173 | export function parseJSON<T = unknown>( 174 | value: unknown, 175 | options: ParseOptions = { strict: true }, 176 | ): T { 177 | return betterJSONParse<T>(value, options); 178 | } 179 | 180 | export default parseJSON; 181 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/tests/performance.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { assert, expect } from "vitest"; 2 | import { createTestSuite } from "../create-test-suite"; 3 | 4 | /** 5 | * This test suite tests the performance of the adapter and logs the results. 6 | */ 7 | export const performanceTestSuite = createTestSuite( 8 | "performance", 9 | {}, 10 | ( 11 | { adapter, generate, cleanup }, 12 | config?: { iterations?: number; userSeedCount?: number; dialect?: string }, 13 | ) => { 14 | const tests = { 15 | create: [] as number[], 16 | update: [] as number[], 17 | delete: [] as number[], 18 | count: [] as number[], 19 | findOne: [] as number[], 20 | findMany: [] as number[], 21 | }; 22 | 23 | const iterations = config?.iterations ?? 10; 24 | const userSeedCount = config?.userSeedCount ?? 15; 25 | 26 | assert( 27 | userSeedCount >= iterations, 28 | "userSeedCount must be greater than iterations", 29 | ); 30 | 31 | const seedUser = async () => { 32 | const user = await generate("user"); 33 | return await adapter.create({ 34 | model: "user", 35 | data: user, 36 | forceAllowId: true, 37 | }); 38 | }; 39 | const seedManyUsers = async () => { 40 | const users = []; 41 | for (let i = 0; i < userSeedCount; i++) { 42 | users.push(await seedUser()); 43 | } 44 | return users; 45 | }; 46 | 47 | const performanceTests = { 48 | create: async () => { 49 | for (let i = 0; i < iterations; i++) { 50 | const start = performance.now(); 51 | await seedUser(); 52 | const end = performance.now(); 53 | tests.create.push(end - start); 54 | } 55 | }, 56 | update: async () => { 57 | const users = await seedManyUsers(); 58 | for (let i = 0; i < iterations; i++) { 59 | const start = performance.now(); 60 | await adapter.update({ 61 | model: "user", 62 | where: [{ field: "id", value: users[i]!.id }], 63 | update: { 64 | name: `user-${i}`, 65 | }, 66 | }); 67 | const end = performance.now(); 68 | tests.update.push(end - start); 69 | } 70 | }, 71 | delete: async () => { 72 | const users = await seedManyUsers(); 73 | for (let i = 0; i < iterations; i++) { 74 | const start = performance.now(); 75 | await adapter.delete({ 76 | model: "user", 77 | where: [{ field: "id", value: users[i]!.id }], 78 | }); 79 | const end = performance.now(); 80 | tests.delete.push(end - start); 81 | } 82 | }, 83 | count: async () => { 84 | const users = await seedManyUsers(); 85 | for (let i = 0; i < iterations; i++) { 86 | const start = performance.now(); 87 | const c = await adapter.count({ 88 | model: "user", 89 | }); 90 | const end = performance.now(); 91 | tests.count.push(end - start); 92 | expect(c).toEqual(users.length); 93 | } 94 | }, 95 | findOne: async () => { 96 | const users = await seedManyUsers(); 97 | for (let i = 0; i < iterations; i++) { 98 | const start = performance.now(); 99 | await adapter.findOne({ 100 | model: "user", 101 | where: [{ field: "id", value: users[i]!.id }], 102 | }); 103 | const end = performance.now(); 104 | tests.findOne.push(end - start); 105 | } 106 | }, 107 | findMany: async () => { 108 | const users = await seedManyUsers(); 109 | for (let i = 0; i < iterations; i++) { 110 | const start = performance.now(); 111 | const result = await adapter.findMany({ 112 | model: "user", 113 | where: [{ field: "name", value: "user", operator: "starts_with" }], 114 | limit: users.length, 115 | }); 116 | const end = performance.now(); 117 | tests.findMany.push(end - start); 118 | expect(result.length).toBe(users.length); 119 | } 120 | }, 121 | }; 122 | 123 | return { 124 | "run performance test": async () => { 125 | for (const test of Object.keys(performanceTests)) { 126 | await performanceTests[test as keyof typeof performanceTests](); 127 | await cleanup(); 128 | } 129 | 130 | // Calculate averages for each test 131 | const averages = Object.entries(tests).reduce( 132 | (acc, [key, values]) => { 133 | const average = 134 | values.length > 0 135 | ? values.reduce((sum, val) => sum + val, 0) / values.length 136 | : 0; 137 | acc[key] = `${average.toFixed(3)}ms`; 138 | return acc; 139 | }, 140 | {} as Record<string, string>, 141 | ); 142 | 143 | console.log(`Performance tests results, counting averages:`); 144 | console.table(averages); 145 | console.log({ 146 | iterations, 147 | userSeedCount, 148 | adapter: adapter.options?.adapterConfig.adapterId, 149 | ...(config?.dialect ? { dialect: config.dialect } : {}), 150 | }); 151 | expect(1).toBe(1); 152 | }, 153 | }; 154 | }, 155 | ); 156 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/one-time-token/one-time-token.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { oneTimeToken } from "."; 4 | import { APIError } from "better-call"; 5 | import { oneTimeTokenClient } from "./client"; 6 | import { defaultKeyHasher } from "./utils"; 7 | 8 | describe("One-time token", async () => { 9 | const { auth, signInWithTestUser, client } = await getTestInstance( 10 | { 11 | plugins: [oneTimeToken()], 12 | }, 13 | { 14 | clientOptions: { 15 | plugins: [oneTimeTokenClient()], 16 | }, 17 | }, 18 | ); 19 | it("should work", async () => { 20 | const { headers } = await signInWithTestUser(); 21 | const response = await auth.api.generateOneTimeToken({ 22 | headers, 23 | }); 24 | expect(response.token).toBeDefined(); 25 | const session = await auth.api.verifyOneTimeToken({ 26 | body: { 27 | token: response.token, 28 | }, 29 | }); 30 | expect(session).toBeDefined(); 31 | const shouldFail = await auth.api 32 | .verifyOneTimeToken({ 33 | body: { 34 | token: response.token, 35 | }, 36 | }) 37 | .catch((e) => e); 38 | expect(shouldFail).toBeInstanceOf(APIError); 39 | }); 40 | 41 | it("should expire", async () => { 42 | const { headers } = await signInWithTestUser(); 43 | const response = await auth.api.generateOneTimeToken({ 44 | headers, 45 | }); 46 | vi.useFakeTimers(); 47 | await vi.advanceTimersByTimeAsync(5 * 60 * 1000); 48 | const shouldFail = await auth.api 49 | .verifyOneTimeToken({ 50 | body: { 51 | token: response.token, 52 | }, 53 | }) 54 | .catch((e) => e); 55 | expect(shouldFail).toBeInstanceOf(APIError); 56 | vi.useRealTimers(); 57 | }); 58 | 59 | it("should work with client", async () => { 60 | const { headers } = await signInWithTestUser(); 61 | const response = await client.oneTimeToken.generate({ 62 | fetchOptions: { 63 | headers, 64 | throw: true, 65 | }, 66 | }); 67 | expect(response.token).toBeDefined(); 68 | const session = await client.oneTimeToken.verify({ 69 | token: response.token, 70 | }); 71 | expect(session.data?.session).toBeDefined(); 72 | }); 73 | 74 | describe("should work with different storeToken options", () => { 75 | describe("hashed", async () => { 76 | const { auth, signInWithTestUser, client } = await getTestInstance( 77 | { 78 | plugins: [ 79 | oneTimeToken({ 80 | storeToken: "hashed", 81 | async generateToken(session, ctx) { 82 | return "123456"; 83 | }, 84 | }), 85 | ], 86 | }, 87 | { 88 | clientOptions: { 89 | plugins: [oneTimeTokenClient()], 90 | }, 91 | }, 92 | ); 93 | const { internalAdapter } = await auth.$context; 94 | 95 | it("should work with hashed", async () => { 96 | const { headers } = await signInWithTestUser(); 97 | const response = await auth.api.generateOneTimeToken({ 98 | headers, 99 | }); 100 | expect(response.token).toBeDefined(); 101 | expect(response.token).toBe("123456"); 102 | 103 | const hashedToken = await defaultKeyHasher(response.token); 104 | const storedToken = await internalAdapter.findVerificationValue( 105 | `one-time-token:${hashedToken}`, 106 | ); 107 | expect(storedToken).toBeDefined(); 108 | 109 | const session = await auth.api.verifyOneTimeToken({ 110 | body: { 111 | token: response.token, 112 | }, 113 | }); 114 | expect(session).toBeDefined(); 115 | expect(session.user.email).toBeDefined(); 116 | }); 117 | }); 118 | 119 | describe("custom hasher", async () => { 120 | const { auth, signInWithTestUser, client } = await getTestInstance({ 121 | plugins: [ 122 | oneTimeToken({ 123 | storeToken: { 124 | type: "custom-hasher", 125 | hash: async (token) => { 126 | return token + "hashed"; 127 | }, 128 | }, 129 | async generateToken(session, ctx) { 130 | return "123456"; 131 | }, 132 | }), 133 | ], 134 | }); 135 | const { internalAdapter } = await auth.$context; 136 | it("should work with custom hasher", async () => { 137 | const { headers } = await signInWithTestUser(); 138 | const response = await auth.api.generateOneTimeToken({ 139 | headers, 140 | }); 141 | expect(response.token).toBeDefined(); 142 | expect(response.token).toBe("123456"); 143 | 144 | const hashedToken = response.token + "hashed"; 145 | const storedToken = await internalAdapter.findVerificationValue( 146 | `one-time-token:${hashedToken}`, 147 | ); 148 | expect(storedToken).toBeDefined(); 149 | 150 | const session = await auth.api.verifyOneTimeToken({ 151 | body: { 152 | token: response.token, 153 | }, 154 | }); 155 | expect(session).toBeDefined(); 156 | }); 157 | }); 158 | }); 159 | }); 160 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/nuxt.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Nuxt Integration 3 | description: Integrate Better Auth with Nuxt. 4 | --- 5 | 6 | Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation). 7 | 8 | ### Create API Route 9 | 10 | We need to mount the handler to an API route. Create a file inside `/server/api/auth` called `[...all].ts` and add the following code: 11 | 12 | ```ts title="server/api/auth/[...all].ts" 13 | import { auth } from "~/lib/auth"; // import your auth config 14 | 15 | export default defineEventHandler((event) => { 16 | return auth.handler(toWebRequest(event)); 17 | }); 18 | ``` 19 | <Callout type="info"> 20 | You can change the path on your better-auth configuration but it's recommended to keep it as `/api/auth/[...all]` 21 | </Callout> 22 | 23 | ### Migrate the database 24 | 25 | Run the following command to create the necessary tables in your database: 26 | 27 | ```bash 28 | npx @better-auth/cli migrate 29 | ``` 30 | 31 | ## Create a client 32 | 33 | Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory. 34 | 35 | ```ts title="auth-client.ts" 36 | import { createAuthClient } from "better-auth/vue" // make sure to import from better-auth/vue 37 | 38 | export const authClient = createAuthClient({ 39 | //you can pass client configuration here 40 | }) 41 | ``` 42 | 43 | Once you have created the client, you can use it to sign up, sign in, and perform other actions. 44 | Some of the actions are reactive. 45 | 46 | ### Example usage 47 | 48 | ```vue title="index.vue" 49 | <script setup lang="ts"> 50 | import { authClient } from "~/lib/client" 51 | const session = authClient.useSession() 52 | </script> 53 | 54 | <template> 55 | <div> 56 | <button v-if="!session?.data" @click="() => authClient.signIn.social({ 57 | provider: 'github' 58 | })"> 59 | Continue with GitHub 60 | </button> 61 | <div> 62 | <pre>{{ session.data }}</pre> 63 | <button v-if="session.data" @click="authClient.signOut()"> 64 | Sign out 65 | </button> 66 | </div> 67 | </div> 68 | </template> 69 | ``` 70 | 71 | ### Server Usage 72 | 73 | The `api` object exported from the auth instance contains all the actions that you can perform on the server. Every endpoint made inside Better Auth is a invocable as a function. Including plugins endpoints. 74 | 75 | **Example: Getting Session on a server API route** 76 | 77 | ```tsx title="server/api/example.ts" 78 | import { auth } from "~/lib/auth"; 79 | 80 | export default defineEventHandler((event) => { 81 | const session = await auth.api.getSession({ 82 | headers: event.headers 83 | }); 84 | 85 | if(session) { 86 | // access the session.session && session.user 87 | } 88 | }); 89 | ``` 90 | 91 | 92 | ### SSR Usage 93 | 94 | If you are using Nuxt with SSR, you can use the `useSession` function in the `setup` function of your page component and pass `useFetch` to make it work with SSR. 95 | 96 | ```vue title="index.vue" 97 | <script setup lang="ts"> 98 | import { authClient } from "~/lib/auth-client"; 99 | 100 | const { data: session } = await authClient.useSession(useFetch); 101 | </script> 102 | 103 | <template> 104 | <p> 105 | {{ session }} 106 | </p> 107 | </template> 108 | ``` 109 | 110 | 111 | ### Middleware 112 | 113 | To add middleware to your Nuxt project, you can use the `useSession` method from the client. 114 | 115 | ```ts title="middleware/auth.global.ts" 116 | import { authClient } from "~/lib/auth-client"; 117 | export default defineNuxtRouteMiddleware(async (to, from) => { 118 | const { data: session } = await authClient.useSession(useFetch); 119 | if (!session.value) { 120 | if (to.path === "/dashboard") { 121 | return navigateTo("/"); 122 | } 123 | } 124 | }); 125 | ``` 126 | 127 | ### Resources & Examples 128 | 129 | - [Nuxt and Nuxt Hub example](https://github.com/atinux/nuxthub-better-auth) on GitHub. 130 | - [NuxtZzle is Nuxt,Drizzle ORM example](https://github.com/leamsigc/nuxt-better-auth-drizzle) on GitHub [preview](https://nuxt-better-auth.giessen.dev/) 131 | - [Nuxt example](https://stackblitz.com/github/better-auth/examples/tree/main/nuxt-example) on StackBlitz. 132 | - [NuxSaaS (Github)](https://github.com/NuxSaaS/NuxSaaS) is a full-stack SaaS Starter Kit that leverages Better Auth for secure and efficient user authentication. [Demo](https://nuxsaas.com/) 133 | - [NuxtOne (Github)](https://github.com/nuxtone/nuxt-one) is a Nuxt-based starter template for building AIaaS (AI-as-a-Service) applications [preview](https://www.one.devv.zone) 134 | ``` -------------------------------------------------------------------------------- /docs/content/docs/concepts/cli.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: CLI 3 | description: Built-in CLI for managing your project. 4 | --- 5 | 6 | Better Auth comes with a built-in CLI to help you manage the database schemas, initialize your project, generate a secret key for your application, and gather diagnostic information about your setup. 7 | 8 | ## Generate 9 | 10 | The `generate` command creates the schema required by Better Auth. If you're using a database adapter like Prisma or Drizzle, this command will generate the right schema for your ORM. If you're using the built-in Kysely adapter, it will generate an SQL file you can run directly on your database. 11 | 12 | ```bash title="Terminal" 13 | npx @better-auth/cli@latest generate 14 | ``` 15 | 16 | ### Options 17 | 18 | - `--output` - Where to save the generated schema. For Prisma, it will be saved in prisma/schema.prisma. For Drizzle, it goes to schema.ts in your project root. For Kysely, it's an SQL file saved as schema.sql in your project root. 19 | - `--config` - The path to your Better Auth config file. By default, the CLI will search for an auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under the `src` directory. 20 | - `--yes` - Skip the confirmation prompt and generate the schema directly. 21 | 22 | 23 | ## Migrate 24 | 25 | The migrate command applies the Better Auth schema directly to your database. This is available if you're using the built-in Kysely adapter. For other adapters, you'll need to apply the schema using your ORM's migration tool. 26 | 27 | ```bash title="Terminal" 28 | npx @better-auth/cli@latest migrate 29 | ``` 30 | 31 | ### Options 32 | 33 | - `--config` - The path to your Better Auth config file. By default, the CLI will search for an auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under the `src` directory. 34 | - `--yes` - Skip the confirmation prompt and apply the schema directly. 35 | 36 | ## Init 37 | 38 | The `init` command allows you to initialize Better Auth in your project. 39 | 40 | ```bash title="Terminal" 41 | npx @better-auth/cli@latest init 42 | ``` 43 | 44 | ### Options 45 | 46 | - `--name` - The name of your application. (defaults to the `name` property in your `package.json`). 47 | - `--framework` - The framework your codebase is using. Currently, the only supported framework is `Next.js`. 48 | - `--plugins` - The plugins you want to use. You can specify multiple plugins by separating them with a comma. 49 | - `--database` - The database you want to use. Currently, the only supported database is `SQLite`. 50 | - `--package-manager` - The package manager you want to use. Currently, the only supported package managers are `npm`, `pnpm`, `yarn`, `bun` (defaults to the manager you used to initialize the CLI). 51 | 52 | ## Info 53 | 54 | The `info` command provides diagnostic information about your Better Auth setup and environment. Useful for debugging and sharing when seeking support. 55 | 56 | ```bash title="Terminal" 57 | npx @better-auth/cli@latest info 58 | ``` 59 | 60 | ### Output 61 | 62 | The command displays: 63 | - **System**: OS, CPU, memory, Node.js version 64 | - **Package Manager**: Detected manager and version 65 | - **Better Auth**: Version and configuration (sensitive data auto-redacted) 66 | - **Frameworks**: Detected frameworks (Next.js, React, Vue, etc.) 67 | - **Databases**: Database clients and ORMs (Prisma, Drizzle, etc.) 68 | 69 | ### Options 70 | 71 | - `--config` - Path to your Better Auth config file 72 | - `--json` - Output as JSON for sharing or programmatic use 73 | 74 | ### Examples 75 | 76 | ```bash 77 | # Basic usage 78 | npx @better-auth/cli@latest info 79 | 80 | # Custom config path 81 | npx @better-auth/cli@latest info --config ./config/auth.ts 82 | 83 | # JSON output 84 | npx @better-auth/cli@latest info --json > auth-info.json 85 | ``` 86 | 87 | Sensitive data like secrets, API keys, and database URLs are automatically replaced with `[REDACTED]` for safe sharing. 88 | 89 | ## Secret 90 | 91 | The CLI also provides a way to generate a secret key for your Better Auth instance. 92 | 93 | ```bash title="Terminal" 94 | npx @better-auth/cli@latest secret 95 | ``` 96 | 97 | ## Common Issues 98 | 99 | **Error: Cannot find module X** 100 | 101 | If you see this error, it means the CLI can't resolve imported modules in your Better Auth config file. We are working on a fix for many of these issues, but in the meantime, you can try the following: 102 | 103 | - Remove any import aliases in your config file and use relative paths instead. After running the CLI, you can revert to using aliases. ``` -------------------------------------------------------------------------------- /docs/components/github-stat.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { kFormatter } from "@/lib/utils"; 2 | 3 | export function GithubStat({ stars }: { stars: string | null }) { 4 | let result = 0; 5 | if (stars) { 6 | result = parseInt(stars?.replace(/,/g, ""), 10); 7 | } else { 8 | return <></>; 9 | } 10 | 11 | return ( 12 | <a 13 | href="https://github.com/better-auth/better-auth" 14 | className="flex mt-4 border border-input shadow-sm hover:bg-accent hover:text-accent-foreground rounded-none h-10 p-5 ml-auto z-50 overflow-hidden items-center text-sm font-medium focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 bg-transparent dark:text-white text-black px-4 py-2 max-w-[14.8rem] whitespace-pre md:flex group relative w-full justify-center gap-2 transition-all duration-300 ease-out hover:ring-black" 15 | > 16 | <span className="absolute right-0 -mt-12 h-32 w-8 translate-x-12 rotate-12 dark:bg-white/60 bg-black/60 opacity-10 transition-all duration-1000 ease-out group-hover:-translate-x-40"></span> 17 | <div className="flex items-center ml-2"> 18 | <svg className="w-4 h-4 fill-current" viewBox="0 0 438.549 438.549"> 19 | <path d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"></path> 20 | </svg> 21 | <span className="ml-2 text-black dark:text-white">Star on GitHub</span> 22 | </div> 23 | <div className="ml-2 flex items-center gap-2 text-sm md:flex"> 24 | <svg 25 | className="w-4 h-4 text-gray-500 transition-all duration-300 group-hover:text-yellow-300" 26 | data-slot="icon" 27 | aria-hidden="true" 28 | fill="currentColor" 29 | viewBox="0 0 24 24" 30 | xmlns="http://www.w3.org/2000/svg" 31 | > 32 | <path 33 | clipRule="evenodd" 34 | d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 35 | fillRule="evenodd" 36 | ></path> 37 | </svg> 38 | <span className="inline-block tabular-nums tracking-wider font-mono font-medium text-black dark:text-white"> 39 | {kFormatter(result)} 40 | </span> 41 | </div> 42 | </a> 43 | ); 44 | } 45 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/jwt/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getWebcryptoSubtle } from "@better-auth/utils"; 2 | import { base64 } from "@better-auth/utils/base64"; 3 | import { joseSecs } from "../../utils/time"; 4 | import type { JwtOptions, Jwk } from "./types"; 5 | import { generateKeyPair, exportJWK } from "jose"; 6 | import { symmetricEncrypt } from "../../crypto"; 7 | import { getJwksAdapter } from "./adapter"; 8 | import type { GenericEndpointContext } from "@better-auth/core"; 9 | 10 | /** 11 | * Converts an expirationTime to ISO seconds expiration time (the format of JWT exp) 12 | * 13 | * See https://github.com/panva/jose/blob/main/src/lib/jwt_claims_set.ts#L245 14 | * 15 | * @param expirationTime - see options.jwt.expirationTime 16 | * @param iat - the iat time to consolidate on 17 | * @returns 18 | */ 19 | export function toExpJWT( 20 | expirationTime: number | Date | string, 21 | iat: number, 22 | ): number { 23 | if (typeof expirationTime === "number") { 24 | return expirationTime; 25 | } else if (expirationTime instanceof Date) { 26 | return Math.floor(expirationTime.getTime() / 1000); 27 | } else { 28 | return iat + joseSecs(expirationTime); 29 | } 30 | } 31 | 32 | async function deriveKey(secretKey: string): Promise<CryptoKey> { 33 | const enc = new TextEncoder(); 34 | const subtle = getWebcryptoSubtle(); 35 | const keyMaterial = await subtle.importKey( 36 | "raw", 37 | enc.encode(secretKey), 38 | { name: "PBKDF2" }, 39 | false, 40 | ["deriveKey"], 41 | ); 42 | 43 | return subtle.deriveKey( 44 | { 45 | name: "PBKDF2", 46 | salt: enc.encode("encryption_salt"), 47 | iterations: 100000, 48 | hash: "SHA-256", 49 | }, 50 | keyMaterial, 51 | { name: "AES-GCM", length: 256 }, 52 | false, 53 | ["encrypt", "decrypt"], 54 | ); 55 | } 56 | 57 | export async function encryptPrivateKey( 58 | privateKey: string, 59 | secretKey: string, 60 | ): Promise<{ encryptedPrivateKey: string; iv: string; authTag: string }> { 61 | const key = await deriveKey(secretKey); // Derive a 32-byte key from the provided secret 62 | const iv = crypto.getRandomValues(new Uint8Array(12)); // 12-byte IV for AES-GCM 63 | 64 | const enc = new TextEncoder(); 65 | const ciphertext = await getWebcryptoSubtle().encrypt( 66 | { 67 | name: "AES-GCM", 68 | iv: iv, 69 | }, 70 | key, 71 | enc.encode(privateKey), 72 | ); 73 | 74 | const encryptedPrivateKey = base64.encode(ciphertext); 75 | const ivBase64 = base64.encode(iv); 76 | 77 | return { 78 | encryptedPrivateKey, 79 | iv: ivBase64, 80 | authTag: encryptedPrivateKey.slice(-16), 81 | }; 82 | } 83 | 84 | export async function decryptPrivateKey( 85 | encryptedPrivate: { 86 | encryptedPrivateKey: string; 87 | iv: string; 88 | authTag: string; 89 | }, 90 | secretKey: string, 91 | ): Promise<string> { 92 | const key = await deriveKey(secretKey); 93 | const { encryptedPrivateKey, iv } = encryptedPrivate; 94 | 95 | const ivBuffer = base64.decode(iv); 96 | const ciphertext = base64.decode(encryptedPrivateKey); 97 | 98 | const decrypted = await getWebcryptoSubtle().decrypt( 99 | { 100 | name: "AES-GCM", 101 | iv: ivBuffer as BufferSource, 102 | }, 103 | key, 104 | ciphertext as BufferSource, 105 | ); 106 | 107 | const dec = new TextDecoder(); 108 | return dec.decode(decrypted); 109 | } 110 | 111 | export async function generateExportedKeyPair(options?: JwtOptions) { 112 | const { alg, ...cfg } = options?.jwks?.keyPairConfig ?? { 113 | alg: "EdDSA", 114 | crv: "Ed25519", 115 | }; 116 | const { publicKey, privateKey } = await generateKeyPair(alg, { 117 | ...cfg, 118 | extractable: true, 119 | }); 120 | 121 | const publicWebKey = await exportJWK(publicKey); 122 | const privateWebKey = await exportJWK(privateKey); 123 | 124 | return { publicWebKey, privateWebKey, alg, cfg }; 125 | } 126 | 127 | /** 128 | * Creates a Jwk on the database 129 | * 130 | * @param ctx 131 | * @param options 132 | * @returns 133 | */ 134 | export async function createJwk( 135 | ctx: GenericEndpointContext, 136 | options?: JwtOptions, 137 | ) { 138 | const { publicWebKey, privateWebKey, alg, cfg } = 139 | await generateExportedKeyPair(options); 140 | 141 | const stringifiedPrivateWebKey = JSON.stringify(privateWebKey); 142 | const privateKeyEncryptionEnabled = 143 | !options?.jwks?.disablePrivateKeyEncryption; 144 | let jwk: Omit<Jwk, "id"> = { 145 | alg, 146 | ...(cfg && "crv" in cfg 147 | ? { 148 | crv: (cfg as { crv: (typeof jwk)["crv"] }).crv, 149 | } 150 | : {}), 151 | publicKey: JSON.stringify(publicWebKey), 152 | privateKey: privateKeyEncryptionEnabled 153 | ? JSON.stringify( 154 | await symmetricEncrypt({ 155 | key: ctx.context.secret, 156 | data: stringifiedPrivateWebKey, 157 | }), 158 | ) 159 | : stringifiedPrivateWebKey, 160 | createdAt: new Date(), 161 | }; 162 | 163 | const adapter = getJwksAdapter(ctx.context.adapter); 164 | const key = await adapter.createJwk(jwk as Jwk); 165 | 166 | return key; 167 | } 168 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/sheet.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SheetPrimitive from "@radix-ui/react-dialog"; 5 | import { Cross2Icon } from "@radix-ui/react-icons"; 6 | import { cva, type VariantProps } from "class-variance-authority"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | 10 | const Sheet = SheetPrimitive.Root; 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger; 13 | 14 | const SheetClose = SheetPrimitive.Close; 15 | 16 | const SheetPortal = SheetPrimitive.Portal; 17 | 18 | const SheetOverlay = ({ 19 | ref, 20 | className, 21 | ...props 22 | }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> & { 23 | ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Overlay>>; 24 | }) => ( 25 | <SheetPrimitive.Overlay 26 | className={cn( 27 | "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", 28 | className, 29 | )} 30 | {...props} 31 | ref={ref} 32 | /> 33 | ); 34 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; 35 | 36 | const sheetVariants = cva( 37 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", 38 | { 39 | variants: { 40 | side: { 41 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 42 | bottom: 43 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 44 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 45 | right: 46 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 47 | }, 48 | }, 49 | defaultVariants: { 50 | side: "right", 51 | }, 52 | }, 53 | ); 54 | 55 | interface SheetContentProps 56 | extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, 57 | VariantProps<typeof sheetVariants> {} 58 | 59 | const SheetContent = ({ 60 | ref, 61 | side = "right", 62 | className, 63 | children, 64 | ...props 65 | }: SheetContentProps & { 66 | ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Content>>; 67 | }) => ( 68 | <SheetPortal> 69 | <SheetOverlay /> 70 | <SheetPrimitive.Content 71 | ref={ref} 72 | className={cn(sheetVariants({ side }), className)} 73 | {...props} 74 | > 75 | <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> 76 | <Cross2Icon className="h-4 w-4" /> 77 | <span className="sr-only">Close</span> 78 | </SheetPrimitive.Close> 79 | {children} 80 | </SheetPrimitive.Content> 81 | </SheetPortal> 82 | ); 83 | SheetContent.displayName = SheetPrimitive.Content.displayName; 84 | 85 | const SheetHeader = ({ 86 | className, 87 | ...props 88 | }: React.HTMLAttributes<HTMLDivElement>) => ( 89 | <div 90 | className={cn( 91 | "flex flex-col space-y-2 text-center sm:text-left", 92 | className, 93 | )} 94 | {...props} 95 | /> 96 | ); 97 | SheetHeader.displayName = "SheetHeader"; 98 | 99 | const SheetFooter = ({ 100 | className, 101 | ...props 102 | }: React.HTMLAttributes<HTMLDivElement>) => ( 103 | <div 104 | className={cn( 105 | "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 106 | className, 107 | )} 108 | {...props} 109 | /> 110 | ); 111 | SheetFooter.displayName = "SheetFooter"; 112 | 113 | const SheetTitle = ({ 114 | ref, 115 | className, 116 | ...props 117 | }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> & { 118 | ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Title>>; 119 | }) => ( 120 | <SheetPrimitive.Title 121 | ref={ref} 122 | className={cn("text-lg font-semibold text-foreground", className)} 123 | {...props} 124 | /> 125 | ); 126 | SheetTitle.displayName = SheetPrimitive.Title.displayName; 127 | 128 | const SheetDescription = ({ 129 | ref, 130 | className, 131 | ...props 132 | }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> & { 133 | ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Description>>; 134 | }) => ( 135 | <SheetPrimitive.Description 136 | ref={ref} 137 | className={cn("text-sm text-muted-foreground", className)} 138 | {...props} 139 | /> 140 | ); 141 | SheetDescription.displayName = SheetPrimitive.Description.displayName; 142 | 143 | export { 144 | Sheet, 145 | SheetPortal, 146 | SheetOverlay, 147 | SheetTrigger, 148 | SheetClose, 149 | SheetContent, 150 | SheetHeader, 151 | SheetFooter, 152 | SheetTitle, 153 | SheetDescription, 154 | }; 155 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/salesforce.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import { BetterAuthError } from "../error"; 3 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 4 | import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2"; 5 | import { logger } from "../env"; 6 | import { refreshAccessToken } from "../oauth2"; 7 | 8 | export interface SalesforceProfile { 9 | sub: string; 10 | user_id: string; 11 | organization_id: string; 12 | preferred_username?: string; 13 | email: string; 14 | email_verified?: boolean; 15 | name: string; 16 | given_name?: string; 17 | family_name?: string; 18 | zoneinfo?: string; 19 | photos?: { 20 | picture?: string; 21 | thumbnail?: string; 22 | }; 23 | } 24 | 25 | export interface SalesforceOptions extends ProviderOptions<SalesforceProfile> { 26 | clientId: string; 27 | environment?: "sandbox" | "production"; 28 | loginUrl?: string; 29 | /** 30 | * Override the redirect URI if auto-detection fails. 31 | * Should match the Callback URL configured in your Salesforce Connected App. 32 | * @example "http://localhost:3000/api/auth/callback/salesforce" 33 | */ 34 | redirectURI?: string; 35 | } 36 | 37 | export const salesforce = (options: SalesforceOptions) => { 38 | const environment = options.environment ?? "production"; 39 | const isSandbox = environment === "sandbox"; 40 | const authorizationEndpoint = options.loginUrl 41 | ? `https://${options.loginUrl}/services/oauth2/authorize` 42 | : isSandbox 43 | ? "https://test.salesforce.com/services/oauth2/authorize" 44 | : "https://login.salesforce.com/services/oauth2/authorize"; 45 | 46 | const tokenEndpoint = options.loginUrl 47 | ? `https://${options.loginUrl}/services/oauth2/token` 48 | : isSandbox 49 | ? "https://test.salesforce.com/services/oauth2/token" 50 | : "https://login.salesforce.com/services/oauth2/token"; 51 | 52 | const userInfoEndpoint = options.loginUrl 53 | ? `https://${options.loginUrl}/services/oauth2/userinfo` 54 | : isSandbox 55 | ? "https://test.salesforce.com/services/oauth2/userinfo" 56 | : "https://login.salesforce.com/services/oauth2/userinfo"; 57 | 58 | return { 59 | id: "salesforce", 60 | name: "Salesforce", 61 | 62 | async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { 63 | if (!options.clientId || !options.clientSecret) { 64 | logger.error( 65 | "Client Id and Client Secret are required for Salesforce. Make sure to provide them in the options.", 66 | ); 67 | throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED"); 68 | } 69 | if (!codeVerifier) { 70 | throw new BetterAuthError("codeVerifier is required for Salesforce"); 71 | } 72 | 73 | const _scopes = options.disableDefaultScope 74 | ? [] 75 | : ["openid", "email", "profile"]; 76 | options.scope && _scopes.push(...options.scope); 77 | scopes && _scopes.push(...scopes); 78 | 79 | return createAuthorizationURL({ 80 | id: "salesforce", 81 | options, 82 | authorizationEndpoint, 83 | scopes: _scopes, 84 | state, 85 | codeVerifier, 86 | redirectURI: options.redirectURI || redirectURI, 87 | }); 88 | }, 89 | 90 | validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { 91 | return validateAuthorizationCode({ 92 | code, 93 | codeVerifier, 94 | redirectURI: options.redirectURI || redirectURI, 95 | options, 96 | tokenEndpoint, 97 | }); 98 | }, 99 | 100 | refreshAccessToken: options.refreshAccessToken 101 | ? options.refreshAccessToken 102 | : async (refreshToken) => { 103 | return refreshAccessToken({ 104 | refreshToken, 105 | options: { 106 | clientId: options.clientId, 107 | clientSecret: options.clientSecret, 108 | }, 109 | tokenEndpoint, 110 | }); 111 | }, 112 | 113 | async getUserInfo(token) { 114 | if (options.getUserInfo) { 115 | return options.getUserInfo(token); 116 | } 117 | 118 | try { 119 | const { data: user } = await betterFetch<SalesforceProfile>( 120 | userInfoEndpoint, 121 | { 122 | headers: { 123 | Authorization: `Bearer ${token.accessToken}`, 124 | }, 125 | }, 126 | ); 127 | 128 | if (!user) { 129 | logger.error("Failed to fetch user info from Salesforce"); 130 | return null; 131 | } 132 | 133 | const userMap = await options.mapProfileToUser?.(user); 134 | 135 | return { 136 | user: { 137 | id: user.user_id, 138 | name: user.name, 139 | email: user.email, 140 | image: user.photos?.picture || user.photos?.thumbnail, 141 | emailVerified: user.email_verified ?? false, 142 | ...userMap, 143 | }, 144 | data: user, 145 | }; 146 | } catch (error) { 147 | logger.error("Failed to fetch user info from Salesforce:", error); 148 | return null; 149 | } 150 | }, 151 | 152 | options, 153 | } satisfies OAuthProvider<SalesforceProfile>; 154 | }; 155 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/one-tap.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: One Tap 3 | description: One Tap plugin for Better Auth 4 | --- 5 | 6 | The One Tap plugin allows users to log in with a single tap using Google's One Tap API. The plugin 7 | provides a simple way to integrate One Tap into your application, handling the client-side and server-side logic for you. 8 | 9 | ## Installation 10 | 11 | ### Add the Server Plugin 12 | 13 | Add the One Tap plugin to your auth configuration: 14 | 15 | ```ts title="auth.ts" 16 | import { betterAuth } from "better-auth"; 17 | import { oneTap } from "better-auth/plugins"; // [!code highlight] 18 | 19 | export const auth = betterAuth({ 20 | plugins: [ // [!code highlight] 21 | oneTap(), // Add the One Tap server plugin // [!code highlight] 22 | ] // [!code highlight] 23 | }); 24 | ``` 25 | 26 | ### Add the Client Plugin 27 | 28 | Add the client plugin and specify where the user should be redirected after sign-in or if additional verification (like 2FA) is needed. 29 | 30 | 31 | ```ts 32 | import { createAuthClient } from "better-auth/client"; 33 | import { oneTapClient } from "better-auth/client/plugins"; 34 | 35 | export const authClient = createAuthClient({ 36 | plugins: [ 37 | oneTapClient({ 38 | clientId: "YOUR_CLIENT_ID", 39 | // Optional client configuration: 40 | autoSelect: false, 41 | cancelOnTapOutside: true, 42 | context: "signin", 43 | additionalOptions: { 44 | // Any extra options for the Google initialize method 45 | }, 46 | // Configure prompt behavior and exponential backoff: 47 | promptOptions: { 48 | baseDelay: 1000, // Base delay in ms (default: 1000) 49 | maxAttempts: 5 // Maximum number of attempts before triggering onPromptNotification (default: 5) 50 | } 51 | }) 52 | ] 53 | }); 54 | ``` 55 | 56 | ### Usage 57 | 58 | To display the One Tap popup, simply call the oneTap method on your auth client: 59 | 60 | ```ts 61 | await authClient.oneTap(); 62 | ``` 63 | 64 | ### Customizing Redirect Behavior 65 | 66 | By default, after a successful login the plugin will hard redirect the user to `/`. You can customize this behavior as follows: 67 | 68 | #### Avoiding a Hard Redirect 69 | 70 | Pass fetchOptions with an onSuccess callback to handle the login response without a page reload: 71 | 72 | ```ts 73 | await authClient.oneTap({ 74 | fetchOptions: { 75 | onSuccess: () => { 76 | // For example, use a router to navigate without a full reload: 77 | router.push("/dashboard"); 78 | } 79 | } 80 | }); 81 | ``` 82 | 83 | #### Specifying a Custom Callback URL 84 | 85 | To perform a hard redirect to a different page after login, use the callbackURL option: 86 | 87 | ```ts 88 | await authClient.oneTap({ 89 | callbackURL: "/dashboard" 90 | }); 91 | ``` 92 | 93 | #### Handling Prompt Dismissals with Exponential Backoff 94 | 95 | If the user dismisses or skips the prompt, the plugin will retry showing the One Tap prompt using exponential backoff based on your configured promptOptions. 96 | 97 | If the maximum number of attempts is reached without a successful sign-in, you can use the onPromptNotification callback to be notified—allowing you to render an alternative UI (e.g., a traditional Google Sign-In button) so users can restart the process manually: 98 | 99 | ```ts 100 | await authClient.oneTap({ 101 | onPromptNotification: (notification) => { 102 | console.warn("Prompt was dismissed or skipped. Consider displaying an alternative sign-in option.", notification); 103 | // Render your alternative UI here 104 | } 105 | }); 106 | ``` 107 | 108 | ### Client Options 109 | 110 | - **clientId**: The client ID for your Google One Tap API. 111 | - **autoSelect**: Automatically select the account if the user is already signed in. Default is false. 112 | - **context**: The context in which the One Tap API should be used (e.g., "signin"). Default is "signin". 113 | - **cancelOnTapOutside**: Cancel the One Tap popup when the user taps outside it. Default is true. 114 | - additionalOptions: Extra options to pass to Google's initialize method as per the [Google Identity Services docs](https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt). 115 | - **promptOptions**: Configuration for the prompt behavior and exponential backoff: 116 | - **baseDelay**: Base delay in milliseconds for retries. Default is 1000. 117 | - **maxAttempts**: Maximum number of prompt attempts before invoking the onPromptNotification callback. Default is 5. 118 | 119 | ### Server Options 120 | 121 | - **disableSignUp**: Disable the sign-up option, allowing only existing users to sign in. Default is `false`. 122 | - **ClientId**: Optionally, pass a client ID here if it is not provided in your social provider configuration. 123 | 124 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/google.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import { decodeJwt } from "jose"; 3 | import { BetterAuthError } from "../error"; 4 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 5 | import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2"; 6 | import { logger } from "../env"; 7 | import { refreshAccessToken } from "../oauth2"; 8 | 9 | export interface GoogleProfile { 10 | aud: string; 11 | azp: string; 12 | email: string; 13 | email_verified: boolean; 14 | exp: number; 15 | /** 16 | * The family name of the user, or last name in most 17 | * Western languages. 18 | */ 19 | family_name: string; 20 | /** 21 | * The given name of the user, or first name in most 22 | * Western languages. 23 | */ 24 | given_name: string; 25 | hd?: string; 26 | iat: number; 27 | iss: string; 28 | jti?: string; 29 | locale?: string; 30 | name: string; 31 | nbf?: number; 32 | picture: string; 33 | sub: string; 34 | } 35 | 36 | export interface GoogleOptions extends ProviderOptions<GoogleProfile> { 37 | clientId: string; 38 | /** 39 | * The access type to use for the authorization code request 40 | */ 41 | accessType?: "offline" | "online"; 42 | /** 43 | * The display mode to use for the authorization code request 44 | */ 45 | display?: "page" | "popup" | "touch" | "wap"; 46 | /** 47 | * The hosted domain of the user 48 | */ 49 | hd?: string; 50 | } 51 | 52 | export const google = (options: GoogleOptions) => { 53 | return { 54 | id: "google", 55 | name: "Google", 56 | async createAuthorizationURL({ 57 | state, 58 | scopes, 59 | codeVerifier, 60 | redirectURI, 61 | loginHint, 62 | display, 63 | }) { 64 | if (!options.clientId || !options.clientSecret) { 65 | logger.error( 66 | "Client Id and Client Secret is required for Google. Make sure to provide them in the options.", 67 | ); 68 | throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED"); 69 | } 70 | if (!codeVerifier) { 71 | throw new BetterAuthError("codeVerifier is required for Google"); 72 | } 73 | const _scopes = options.disableDefaultScope 74 | ? [] 75 | : ["email", "profile", "openid"]; 76 | options.scope && _scopes.push(...options.scope); 77 | scopes && _scopes.push(...scopes); 78 | const url = await createAuthorizationURL({ 79 | id: "google", 80 | options, 81 | authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth", 82 | scopes: _scopes, 83 | state, 84 | codeVerifier, 85 | redirectURI, 86 | prompt: options.prompt, 87 | accessType: options.accessType, 88 | display: display || options.display, 89 | loginHint, 90 | hd: options.hd, 91 | additionalParams: { 92 | include_granted_scopes: "true", 93 | }, 94 | }); 95 | return url; 96 | }, 97 | validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { 98 | return validateAuthorizationCode({ 99 | code, 100 | codeVerifier, 101 | redirectURI, 102 | options, 103 | tokenEndpoint: "https://oauth2.googleapis.com/token", 104 | }); 105 | }, 106 | refreshAccessToken: options.refreshAccessToken 107 | ? options.refreshAccessToken 108 | : async (refreshToken) => { 109 | return refreshAccessToken({ 110 | refreshToken, 111 | options: { 112 | clientId: options.clientId, 113 | clientKey: options.clientKey, 114 | clientSecret: options.clientSecret, 115 | }, 116 | tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token", 117 | }); 118 | }, 119 | async verifyIdToken(token, nonce) { 120 | if (options.disableIdTokenSignIn) { 121 | return false; 122 | } 123 | if (options.verifyIdToken) { 124 | return options.verifyIdToken(token, nonce); 125 | } 126 | const googlePublicKeyUrl = `https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token}`; 127 | const { data: tokenInfo } = await betterFetch<{ 128 | aud: string; 129 | iss: string; 130 | email: string; 131 | email_verified: boolean; 132 | name: string; 133 | picture: string; 134 | sub: string; 135 | }>(googlePublicKeyUrl); 136 | if (!tokenInfo) { 137 | return false; 138 | } 139 | const isValid = 140 | tokenInfo.aud === options.clientId && 141 | (tokenInfo.iss === "https://accounts.google.com" || 142 | tokenInfo.iss === "accounts.google.com"); 143 | return isValid; 144 | }, 145 | async getUserInfo(token) { 146 | if (options.getUserInfo) { 147 | return options.getUserInfo(token); 148 | } 149 | if (!token.idToken) { 150 | return null; 151 | } 152 | const user = decodeJwt(token.idToken) as GoogleProfile; 153 | const userMap = await options.mapProfileToUser?.(user); 154 | return { 155 | user: { 156 | id: user.sub, 157 | name: user.name, 158 | email: user.email, 159 | image: user.picture, 160 | emailVerified: user.email_verified, 161 | ...userMap, 162 | }, 163 | data: user, 164 | }; 165 | }, 166 | options, 167 | } satisfies OAuthProvider<GoogleProfile>; 168 | }; 169 | ``` -------------------------------------------------------------------------------- /docs/content/docs/reference/telemetry.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Telemetry 3 | description: Better Auth now collects anonymous telemetry data about general usage. 4 | --- 5 | 6 | Better Auth collects anonymous usage data to help us improve the project. This is optional, transparent, and disabled by default. 7 | 8 | ## Why is telemetry collected? 9 | 10 | Since v1.3.5, Better Auth collects anonymous telemetry data about general usage if enabled. 11 | 12 | Telemetry data helps us understand how Better Auth is being used across different environments so we can improve performance, prioritize features, and fix issues more effectively. It guides our decisions on performance optimizations, feature development, and bug fixes. All data is collected completely anonymously and with privacy in mind, and users can opt out at any time. We strive to keep what we collect as transparent as possible. 13 | 14 | ## What is being collected? 15 | 16 | The following data points may be reported. Everything is anonymous and intended for aggregate insights only. 17 | 18 | - **Anonymous identifier**: A non-reversible hash derived from your project (`package.json` name and optionally `baseURL`). This lets us de‑duplicate events per project without knowing who you are. 19 | - **Runtime**: `{ name: "node" | "bun" | "deno", version }`. 20 | - **Environment**: one of `development`, `production`, `test`, or `ci`. 21 | - **Framework (if detected)**: `{ name, version }` for frameworks like Next.js, Nuxt, Remix, Astro, SvelteKit, etc. 22 | - **Database (if detected)**: `{ name, version }` for integrations like PostgreSQL, MySQL, SQLite, Prisma, Drizzle, MongoDB, etc. 23 | - **System info**: platform, OS release, architecture, CPU count/model/speed, total memory, and flags like `isDocker`, `isWSL`, `isTTY`. 24 | - **Package manager**: `{ name, version }` derived from the npm user agent. 25 | - **Redacted auth config snapshot**: A minimized, privacy‑preserving view of your `betterAuth` options produced by `getTelemetryAuthConfig`. 26 | 27 | We also collect anonymous telemetry from the CLI: 28 | 29 | - **CLI generate (`cli_generate`)**: outcome `generated | overwritten | appended | no_changes | aborted` plus redacted config. 30 | - **CLI migrate (`cli_migrate`)**: outcome `migrated | no_changes | aborted | unsupported_adapter` plus adapter id (when relevant) and redacted config. 31 | 32 | 33 | <Callout type="info"> 34 | You can audit telemetry locally by setting the `BETTER_AUTH_TELEMETRY_DEBUG=1` environment variable when running your project or by setting `telemetry: { debug: true }` in your auth config. In this debug mode, telemetry events are logged only to the console. 35 | 36 | ```ts title="auth.ts" 37 | export const auth = betterAuth({ 38 | // [!code highlight] 39 | telemetry: { // [!code highlight] 40 | debug: true // [!code highlight] 41 | } // [!code highlight] 42 | }); 43 | ``` 44 | </Callout> 45 | 46 | ## How is my data protected? 47 | 48 | All collected data is fully anonymous and only useful in aggregate. It cannot be traced back to any individual source and is accessible only to a small group of core Better Auth maintainers to guide roadmap decisions. 49 | 50 | - **No PII or secrets**: We do not collect emails, usernames, tokens, secrets, client IDs, client secrets, or database URLs. 51 | - **No full config**: We never send your full `betterAuth` configuration. Instead we send a reduced, redacted snapshot of non‑sensitive toggles and counts. 52 | - **Redaction by design**: See [detect-auth-config.ts](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/telemetry/detectors/detect-auth-config.ts) in the Better Auth source for the exact shape of what is included. It purposely converts sensitive values to booleans, counts, or generic identifiers. 53 | 54 | ## How can I enable it? 55 | 56 | You can enable telemetry collection in your auth config or by setting an environment variable. 57 | 58 | - Via your auth config. 59 | 60 | ```ts title="auth.ts" 61 | export const auth = betterAuth({ 62 | // [!code highlight] 63 | telemetry: { // [!code highlight] 64 | enabled: true// [!code highlight] 65 | } // [!code highlight] 66 | }); 67 | ``` 68 | 69 | - Via an environment variable. 70 | 71 | ```txt title=".env" 72 | # Enable telemetry 73 | BETTER_AUTH_TELEMETRY=1 74 | 75 | # Disable telemetry 76 | BETTER_AUTH_TELEMETRY=0 77 | ``` 78 | 79 | 80 | ### When is telemetry sent? 81 | 82 | - On `betterAuth` initialization (`type: "init"`). 83 | - On CLI actions: `generate` and `migrate` as described above. 84 | 85 | Telemetry is disabled automatically in tests (`NODE_ENV=test`) unless explicitly overridden by internal tooling. 86 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/path-to-object.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { 2 | BetterFetchOption, 3 | BetterFetchResponse, 4 | } from "@better-fetch/fetch"; 5 | import type { InputContext, Endpoint, StandardSchemaV1 } from "better-call"; 6 | import type { 7 | HasRequiredKeys, 8 | Prettify, 9 | UnionToIntersection, 10 | } from "../types/helper"; 11 | import type { 12 | InferAdditionalFromClient, 13 | InferSessionFromClient, 14 | InferUserFromClient, 15 | } from "./types"; 16 | import type { BetterAuthClientOptions } from "@better-auth/core"; 17 | 18 | export type CamelCase<S extends string> = 19 | S extends `${infer P1}-${infer P2}${infer P3}` 20 | ? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}` 21 | : Lowercase<S>; 22 | 23 | export type PathToObject< 24 | T extends string, 25 | Fn extends (...args: any[]) => any, 26 | > = T extends `/${infer Segment}/${infer Rest}` 27 | ? { [K in CamelCase<Segment>]: PathToObject<`/${Rest}`, Fn> } 28 | : T extends `/${infer Segment}` 29 | ? { [K in CamelCase<Segment>]: Fn } 30 | : never; 31 | 32 | export type InferSignUpEmailCtx< 33 | ClientOpts extends BetterAuthClientOptions, 34 | FetchOptions extends BetterFetchOption, 35 | > = { 36 | email: string; 37 | name: string; 38 | password: string; 39 | image?: string; 40 | callbackURL?: string; 41 | fetchOptions?: FetchOptions; 42 | } & UnionToIntersection<InferAdditionalFromClient<ClientOpts, "user", "input">>; 43 | 44 | export type InferUserUpdateCtx< 45 | ClientOpts extends BetterAuthClientOptions, 46 | FetchOptions extends BetterFetchOption, 47 | > = { 48 | image?: string | null; 49 | name?: string; 50 | fetchOptions?: FetchOptions; 51 | } & Partial< 52 | UnionToIntersection<InferAdditionalFromClient<ClientOpts, "user", "input">> 53 | >; 54 | 55 | export type InferCtx< 56 | C extends InputContext<any, any>, 57 | FetchOptions extends BetterFetchOption, 58 | > = C["body"] extends Record<string, any> 59 | ? C["body"] & { 60 | fetchOptions?: FetchOptions; 61 | } 62 | : C["query"] extends Record<string, any> 63 | ? { 64 | query: C["query"]; 65 | fetchOptions?: FetchOptions; 66 | } 67 | : C["query"] extends Record<string, any> | undefined 68 | ? { 69 | query?: C["query"]; 70 | fetchOptions?: FetchOptions; 71 | } 72 | : { 73 | fetchOptions?: FetchOptions; 74 | }; 75 | 76 | export type MergeRoutes<T> = UnionToIntersection<T>; 77 | 78 | export type InferRoute< 79 | API, 80 | COpts extends BetterAuthClientOptions, 81 | > = API extends Record<string, infer T> 82 | ? T extends Endpoint 83 | ? T["options"]["metadata"] extends 84 | | { 85 | isAction: false; 86 | } 87 | | { 88 | SERVER_ONLY: true; 89 | } 90 | ? {} 91 | : PathToObject< 92 | T["path"], 93 | T extends (ctx: infer C) => infer R 94 | ? C extends InputContext<any, any> 95 | ? < 96 | FetchOptions extends BetterFetchOption< 97 | Partial<C["body"]> & Record<string, any>, 98 | Partial<C["query"]> & Record<string, any>, 99 | C["params"] 100 | >, 101 | >( 102 | ...data: HasRequiredKeys< 103 | InferCtx<C, FetchOptions> 104 | > extends true 105 | ? [ 106 | Prettify< 107 | T["path"] extends `/sign-up/email` 108 | ? InferSignUpEmailCtx<COpts, FetchOptions> 109 | : InferCtx<C, FetchOptions> 110 | >, 111 | FetchOptions?, 112 | ] 113 | : [ 114 | Prettify< 115 | T["path"] extends `/update-user` 116 | ? InferUserUpdateCtx<COpts, FetchOptions> 117 | : InferCtx<C, FetchOptions> 118 | >?, 119 | FetchOptions?, 120 | ] 121 | ) => Promise< 122 | BetterFetchResponse< 123 | T["options"]["metadata"] extends { 124 | CUSTOM_SESSION: boolean; 125 | } 126 | ? NonNullable<Awaited<R>> 127 | : T["path"] extends "/get-session" 128 | ? { 129 | user: InferUserFromClient<COpts>; 130 | session: InferSessionFromClient<COpts>; 131 | } | null 132 | : NonNullable<Awaited<R>>, 133 | T["options"]["error"] extends StandardSchemaV1 134 | ? // InferOutput 135 | NonNullable< 136 | T["options"]["error"]["~standard"]["types"] 137 | >["output"] 138 | : { 139 | code?: string; 140 | message?: string; 141 | }, 142 | FetchOptions["throw"] extends true 143 | ? true 144 | : COpts["fetchOptions"] extends { throw: true } 145 | ? true 146 | : false 147 | > 148 | > 149 | : never 150 | : never 151 | > 152 | : {} 153 | : never; 154 | 155 | export type InferRoutes< 156 | API extends Record<string, Endpoint>, 157 | ClientOpts extends BetterAuthClientOptions, 158 | > = MergeRoutes<InferRoute<API, ClientOpts>>; 159 | 160 | export type ProxyRequest = { 161 | options?: BetterFetchOption<any, any>; 162 | query?: any; 163 | [key: string]: any; 164 | }; 165 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/community-plugins.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Community Plugins 3 | description: A list of recommended community plugins. 4 | --- 5 | 6 | This page showcases a list of recommended community made plugins. 7 | 8 | We encourage you to create custom plugins and maybe get added to the list! 9 | 10 | To create your own custom plugin, get started by reading our [plugins documentation](/docs/concepts/plugins). And if you want to share your plugin with the community, please open a pull request to add it to this list. 11 | 12 | | <div className="w-[200px]">Plugin</div> | Description | <div className="w-[150px]">Author</div> | 13 | | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 14 | | [@dymo-api/better-auth](https://github.com/TPEOficial/dymo-api-better-auth) | Sign Up Protection and validation of disposable emails (the world's largest database with nearly 14 million entries). | <img src="https://github.com/TPEOficial.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [TPEOficial](https://github.com/TPEOficial) | 15 | | [better-auth-harmony](https://github.com/gekorm/better-auth-harmony/) | Email & phone normalization and additional validation, blocking over 55,000 temporary email domains. | <img src="https://github.com/GeKorm.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [GeKorm](https://github.com/GeKorm) | 16 | | [validation-better-auth](https://github.com/Daanish2003/validation-better-auth) | Validate API request using any validation library (e.g., Zod, Yup) | <img src="https://github.com/Daanish2003.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [Daanish2003](https://github.com/Daanish2003) | 17 | | [better-auth-localization](https://github.com/marcellosso/better-auth-localization) | Localize and customize better-auth messages with easy translation and message override support. | <img src="https://github.com/marcellosso.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [marcellosso](https://github.com/marcellosso) | 18 | | [better-auth-attio-plugin](https://github.com/tobimori/better-auth-attio-plugin) | Sync your products Better Auth users & workspaces with Attio | <img src="https://github.com/tobimori.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [tobimori](https://github.com/tobimori) | 19 | | [better-auth-cloudflare](https://github.com/zpg6/better-auth-cloudflare) | Seamlessly integrate with Cloudflare Workers, D1, Hyperdrive, KV, R2, and geolocation services. Includes CLI for project generation, automated resource provisioning on Cloudflare, and database migrations. Supports Next.js, Hono, and more! | <img src="https://github.com/zpg6.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [zpg6](https://github.com/zpg6) | 20 | | [expo-better-auth-passkey](https://github.com/kevcube/expo-better-auth-passkey) | Better-auth client plugin for using passkeys on mobile platforms in expo apps. Supports iOS, macOS, Android (and web!) by wrapping the existing better-auth passkey client plugin. | <img src="https://github.com/kevcube.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [kevcube](https://github.com/kevcube) | 21 | | [better-auth-credentials-plugin](https://github.com/erickweil/better-auth-credentials-plugin) | LDAP authentication plugin for Better Auth. | <img src="https://github.com/erickweil.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [erickweil](https://github.com/erickweil) | 22 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "@/components/ui/button"; 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root; 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal; 14 | 15 | const AlertDialogOverlay = ({ 16 | ref, 17 | className, 18 | ...props 19 | }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> & { 20 | ref: React.RefObject<React.ElementRef<typeof AlertDialogPrimitive.Overlay>>; 21 | }) => ( 22 | <AlertDialogPrimitive.Overlay 23 | className={cn( 24 | "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", 25 | className, 26 | )} 27 | {...props} 28 | ref={ref} 29 | /> 30 | ); 31 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 32 | 33 | const AlertDialogContent = ({ 34 | ref, 35 | className, 36 | ...props 37 | }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & { 38 | ref: React.RefObject<React.ElementRef<typeof AlertDialogPrimitive.Content>>; 39 | }) => ( 40 | <AlertDialogPortal> 41 | <AlertDialogOverlay /> 42 | <AlertDialogPrimitive.Content 43 | ref={ref} 44 | className={cn( 45 | "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", 46 | className, 47 | )} 48 | {...props} 49 | /> 50 | </AlertDialogPortal> 51 | ); 52 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 53 | 54 | const AlertDialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes<HTMLDivElement>) => ( 58 | <div 59 | className={cn( 60 | "flex flex-col space-y-2 text-center sm:text-left", 61 | className, 62 | )} 63 | {...props} 64 | /> 65 | ); 66 | AlertDialogHeader.displayName = "AlertDialogHeader"; 67 | 68 | const AlertDialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes<HTMLDivElement>) => ( 72 | <div 73 | className={cn( 74 | "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 75 | className, 76 | )} 77 | {...props} 78 | /> 79 | ); 80 | AlertDialogFooter.displayName = "AlertDialogFooter"; 81 | 82 | const AlertDialogTitle = ({ 83 | ref, 84 | className, 85 | ...props 86 | }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> & { 87 | ref: React.RefObject<React.ElementRef<typeof AlertDialogPrimitive.Title>>; 88 | }) => ( 89 | <AlertDialogPrimitive.Title 90 | ref={ref} 91 | className={cn("text-lg font-semibold", className)} 92 | {...props} 93 | /> 94 | ); 95 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 96 | 97 | const AlertDialogDescription = ({ 98 | ref, 99 | className, 100 | ...props 101 | }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> & { 102 | ref: React.RefObject< 103 | React.ElementRef<typeof AlertDialogPrimitive.Description> 104 | >; 105 | }) => ( 106 | <AlertDialogPrimitive.Description 107 | ref={ref} 108 | className={cn("text-sm text-muted-foreground", className)} 109 | {...props} 110 | /> 111 | ); 112 | AlertDialogDescription.displayName = 113 | AlertDialogPrimitive.Description.displayName; 114 | 115 | const AlertDialogAction = ({ 116 | ref, 117 | className, 118 | ...props 119 | }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> & { 120 | ref: React.RefObject<React.ElementRef<typeof AlertDialogPrimitive.Action>>; 121 | }) => ( 122 | <AlertDialogPrimitive.Action 123 | ref={ref} 124 | className={cn(buttonVariants(), className)} 125 | {...props} 126 | /> 127 | ); 128 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 129 | 130 | const AlertDialogCancel = ({ 131 | ref, 132 | className, 133 | ...props 134 | }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> & { 135 | ref: React.RefObject<React.ElementRef<typeof AlertDialogPrimitive.Cancel>>; 136 | }) => ( 137 | <AlertDialogPrimitive.Cancel 138 | ref={ref} 139 | className={cn( 140 | buttonVariants({ variant: "outline" }), 141 | "mt-2 sm:mt-0", 142 | className, 143 | )} 144 | {...props} 145 | /> 146 | ); 147 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 148 | 149 | export { 150 | AlertDialog, 151 | AlertDialogPortal, 152 | AlertDialogOverlay, 153 | AlertDialogTrigger, 154 | AlertDialogContent, 155 | AlertDialogHeader, 156 | AlertDialogFooter, 157 | AlertDialogTitle, 158 | AlertDialogDescription, 159 | AlertDialogAction, 160 | AlertDialogCancel, 161 | }; 162 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as DialogPrimitive from "@rn-primitives/dialog"; 2 | import * as React from "react"; 3 | import { Platform, StyleSheet, View, type ViewProps } from "react-native"; 4 | import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; 5 | import { X } from "@/lib/icons/X"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Dialog = DialogPrimitive.Root; 9 | 10 | const DialogTrigger = DialogPrimitive.Trigger; 11 | 12 | const DialogPortal = DialogPrimitive.Portal; 13 | 14 | const DialogClose = DialogPrimitive.Close; 15 | 16 | const DialogOverlayWeb = React.forwardRef< 17 | DialogPrimitive.OverlayRef, 18 | DialogPrimitive.OverlayProps 19 | >(({ className, ...props }, ref) => { 20 | const { open } = DialogPrimitive.useRootContext(); 21 | return ( 22 | <DialogPrimitive.Overlay 23 | className={cn( 24 | "bg-black/80 flex justify-center items-center p-2 absolute top-0 right-0 bottom-0 left-0", 25 | open 26 | ? "web:animate-in web:fade-in-0" 27 | : "web:animate-out web:fade-out-0", 28 | className, 29 | )} 30 | {...props} 31 | ref={ref} 32 | /> 33 | ); 34 | }); 35 | 36 | DialogOverlayWeb.displayName = "DialogOverlayWeb"; 37 | 38 | const DialogOverlayNative = React.forwardRef< 39 | DialogPrimitive.OverlayRef, 40 | DialogPrimitive.OverlayProps 41 | >(({ className, children, ...props }, ref) => { 42 | return ( 43 | <DialogPrimitive.Overlay 44 | style={StyleSheet.absoluteFill} 45 | className={cn( 46 | "flex bg-black/80 justify-center items-center p-2", 47 | className, 48 | )} 49 | {...props} 50 | ref={ref} 51 | > 52 | <Animated.View 53 | entering={FadeIn.duration(150)} 54 | exiting={FadeOut.duration(150)} 55 | > 56 | <>{children}</> 57 | </Animated.View> 58 | </DialogPrimitive.Overlay> 59 | ); 60 | }); 61 | 62 | DialogOverlayNative.displayName = "DialogOverlayNative"; 63 | 64 | const DialogOverlay = Platform.select({ 65 | web: DialogOverlayWeb, 66 | default: DialogOverlayNative, 67 | }); 68 | 69 | const DialogContent = React.forwardRef< 70 | DialogPrimitive.ContentRef, 71 | DialogPrimitive.ContentProps & { portalHost?: string } 72 | >(({ className, children, portalHost, ...props }, ref) => { 73 | const { open } = DialogPrimitive.useRootContext(); 74 | return ( 75 | <DialogPortal hostName={portalHost}> 76 | <DialogOverlay> 77 | <DialogPrimitive.Content 78 | ref={ref} 79 | className={cn( 80 | "max-w-lg gap-4 border border-border web:cursor-default bg-background p-6 shadow-lg web:duration-200 rounded-lg", 81 | open 82 | ? "web:animate-in web:fade-in-0 web:zoom-in-95" 83 | : "web:animate-out web:fade-out-0 web:zoom-out-95", 84 | className, 85 | )} 86 | {...props} 87 | > 88 | {children} 89 | <DialogPrimitive.Close 90 | className={ 91 | "absolute right-4 top-4 p-0.5 web:group rounded-sm opacity-70 web:ring-offset-background web:transition-opacity web:hover:opacity-100 web:focus:outline-none web:focus:ring-2 web:focus:ring-ring web:focus:ring-offset-2 web:disabled:pointer-events-none" 92 | } 93 | > 94 | <X 95 | size={Platform.OS === "web" ? 16 : 18} 96 | className={cn( 97 | "text-muted-foreground", 98 | open && "text-accent-foreground", 99 | )} 100 | /> 101 | </DialogPrimitive.Close> 102 | </DialogPrimitive.Content> 103 | </DialogOverlay> 104 | </DialogPortal> 105 | ); 106 | }); 107 | DialogContent.displayName = DialogPrimitive.Content.displayName; 108 | 109 | const DialogHeader = ({ className, ...props }: ViewProps) => ( 110 | <View 111 | className={cn("flex flex-col gap-1.5 text-center sm:text-left", className)} 112 | {...props} 113 | /> 114 | ); 115 | DialogHeader.displayName = "DialogHeader"; 116 | 117 | const DialogFooter = ({ className, ...props }: ViewProps) => ( 118 | <View 119 | className={cn( 120 | "flex flex-col-reverse sm:flex-row sm:justify-end gap-2", 121 | className, 122 | )} 123 | {...props} 124 | /> 125 | ); 126 | DialogFooter.displayName = "DialogFooter"; 127 | 128 | const DialogTitle = React.forwardRef< 129 | DialogPrimitive.TitleRef, 130 | DialogPrimitive.TitleProps 131 | >(({ className, ...props }, ref) => ( 132 | <DialogPrimitive.Title 133 | ref={ref} 134 | className={cn( 135 | "text-lg native:text-xl text-foreground font-semibold leading-none tracking-tight", 136 | className, 137 | )} 138 | {...props} 139 | /> 140 | )); 141 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 142 | 143 | const DialogDescription = React.forwardRef< 144 | DialogPrimitive.DescriptionRef, 145 | DialogPrimitive.DescriptionProps 146 | >(({ className, ...props }, ref) => ( 147 | <DialogPrimitive.Description 148 | ref={ref} 149 | className={cn("text-sm native:text-base text-muted-foreground", className)} 150 | {...props} 151 | /> 152 | )); 153 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 154 | 155 | export { 156 | Dialog, 157 | DialogClose, 158 | DialogContent, 159 | DialogDescription, 160 | DialogFooter, 161 | DialogHeader, 162 | DialogOverlay, 163 | DialogPortal, 164 | DialogTitle, 165 | DialogTrigger, 166 | }; 167 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/oidc-provider/ui.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const authorizeHTML = ({ 2 | scopes, 3 | clientIcon, 4 | clientName, 5 | redirectURI, 6 | cancelURI, 7 | }: { 8 | scopes: string[]; 9 | clientIcon?: string; 10 | clientName: string; 11 | redirectURI: string; 12 | cancelURI: string; 13 | clientMetadata?: Record<string, any>; 14 | }) => `<!DOCTYPE html> 15 | <html lang="en"> 16 | <head> 17 | <meta charset="UTF-8"> 18 | <meta clientName="viewport" content="width=device-width, initial-scale=1.0"> 19 | <title>Authorize Application</title> 20 | <style> 21 | :root { 22 | --bg-color: #000000; 23 | --card-color: #1a1a1a; 24 | --text-primary: #ffffff; 25 | --text-secondary: #b0b0b0; 26 | --border-color: #333333; 27 | --button-color: #ffffff; 28 | --button-text: #000000; 29 | } 30 | body { 31 | font-family: 'Inter', 'Helvetica', 'Arial', sans-serif; 32 | background-color: var(--bg-color); 33 | color: var(--text-primary); 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | min-height: 100vh; 38 | margin: 0; 39 | padding: 20px; 40 | box-sizing: border-box; 41 | } 42 | .authorize-container { 43 | background-color: var(--card-color); 44 | border: 1px solid var(--border-color); 45 | padding: 32px; 46 | width: 100%; 47 | max-width: 420px; 48 | box-shadow: 0 8px 24px rgba(255,255,255,0.1); 49 | } 50 | .app-info { 51 | display: flex; 52 | align-items: center; 53 | margin-bottom: 24px; 54 | } 55 | .app-clientIcon { 56 | width: 64px; 57 | height: 64px; 58 | margin-right: 16px; 59 | object-fit: cover; 60 | } 61 | .app-clientName { 62 | font-size: 24px; 63 | font-weight: 700; 64 | } 65 | .permissions-list { 66 | background-color: rgba(255, 255, 255, 0.05); 67 | border: 1px solid var(--border-color); 68 | padding: 16px; 69 | margin-bottom: 24px; 70 | } 71 | .permissions-list h3 { 72 | margin-top: 0; 73 | font-size: 16px; 74 | color: var(--text-secondary); 75 | margin-bottom: 12px; 76 | } 77 | .permissions-list ul { 78 | list-style-type: none; 79 | padding: 0; 80 | margin: 0; 81 | } 82 | .permissions-list li { 83 | margin-bottom: 8px; 84 | display: flex; 85 | align-items: center; 86 | } 87 | .permissions-list li::before { 88 | content: "•"; 89 | color: var(--text-primary); 90 | font-size: 18px; 91 | margin-right: 8px; 92 | } 93 | .buttons { 94 | display: flex; 95 | justify-content: flex-end; 96 | gap: 12px; 97 | } 98 | .button { 99 | padding: 10px 20px; 100 | border: none; 101 | font-size: 14px; 102 | font-weight: 600; 103 | cursor: pointer; 104 | transition: all 0.2s ease; 105 | } 106 | .authorize { 107 | background-color: var(--button-color); 108 | color: var(--button-text); 109 | } 110 | .authorize:hover { 111 | opacity: 0.9; 112 | } 113 | .cancel { 114 | background-color: transparent; 115 | color: var(--text-secondary); 116 | border: 1px solid var(--text-secondary); 117 | } 118 | .cancel:hover { 119 | background-color: rgba(255, 255, 255, 0.1); 120 | } 121 | </style> 122 | </head> 123 | <body> 124 | <div class="authorize-container"> 125 | <div class="app-info"> 126 | <img src="${clientIcon || ""}" alt="${clientName} clientIcon" class="app-clientIcon"> 127 | <span class="app-clientName">${clientName}</span> 128 | </div> 129 | <p>${clientName} would like permission to access your account</p> 130 | <div class="permissions-list"> 131 | <h3>This will allow ${clientName} to:</h3> 132 | <ul> 133 | ${scopes.map((scope) => `<li>${scope}</li>`).join("")} 134 | </ul> 135 | </div> 136 | <div class="buttons"> 137 | <a href="${cancelURI}" class="button cancel">Cancel</a> 138 | <a href="${redirectURI}" class="button authorize">Authorize</a> 139 | </div> 140 | </div> 141 | </body> 142 | </html>`; 143 | ```