This is page 41 of 68. 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-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.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 │ │ │ │ ├── sso │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sso.test.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 │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/content/docs/plugins/oidc-provider.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: OIDC Provider 3 | description: Open ID Connect plugin for Better Auth that allows you to have your own OIDC provider. 4 | --- 5 | 6 | The **OIDC Provider Plugin** enables you to build and manage your own OpenID Connect (OIDC) provider, granting full control over user authentication without relying on third-party services like Okta or Azure AD. It also allows other services to authenticate users through your OIDC provider. 7 | 8 | **Key Features**: 9 | 10 | - **Client Registration**: Register clients to authenticate with your OIDC provider. 11 | - **Dynamic Client Registration**: Allow clients to register dynamically. 12 | - **Trusted Clients**: Configure hard-coded trusted clients with optional consent bypass. 13 | - **Authorization Code Flow**: Support the Authorization Code Flow. 14 | - **Public Clients**: Support public clients for SPA, mobile apps, CLI tools, etc. 15 | - **JWKS Endpoint**: Publish a JWKS endpoint to allow clients to verify tokens. (Not fully implemented) 16 | - **Refresh Tokens**: Issue refresh tokens and handle access token renewal using the `refresh_token` grant. 17 | - **OAuth Consent**: Implement OAuth consent screens for user authorization, with an option to bypass consent for trusted applications. 18 | - **UserInfo Endpoint**: Provide a UserInfo endpoint for clients to retrieve user details. 19 | 20 | <Callout type="warn"> 21 | This plugin is in active development and may not be suitable for production use. Please report any issues or bugs on [GitHub](https://github.com/better-auth/better-auth). 22 | </Callout> 23 | 24 | ## Installation 25 | 26 | <Steps> 27 | <Step> 28 | ### Mount the Plugin 29 | 30 | Add the OIDC plugin to your auth config. See [OIDC Configuration](#oidc-configuration) on how to configure the plugin. 31 | 32 | ```ts title="auth.ts" 33 | import { betterAuth } from "better-auth"; 34 | import { oidcProvider } from "better-auth/plugins"; 35 | 36 | const auth = betterAuth({ 37 | plugins: [oidcProvider({ 38 | loginPage: "/sign-in", // path to the login page 39 | // ...other options 40 | })] 41 | }) 42 | ``` 43 | </Step> 44 | 45 | <Step> 46 | ### Migrate the Database 47 | 48 | Run the migration or generate the schema to add the necessary fields and tables to the database. 49 | 50 | <Tabs items={["migrate", "generate"]}> 51 | <Tab value="migrate"> 52 | ```bash 53 | npx @better-auth/cli migrate 54 | ``` 55 | </Tab> 56 | <Tab value="generate"> 57 | ```bash 58 | npx @better-auth/cli generate 59 | ``` 60 | </Tab> 61 | </Tabs> 62 | See the [Schema](#schema) section to add the fields manually. 63 | </Step> 64 | 65 | <Step> 66 | ### Add the Client Plugin 67 | 68 | Add the OIDC client plugin to your auth client config. 69 | 70 | ```ts 71 | import { createAuthClient } from "better-auth/client"; 72 | import { oidcClient } from "better-auth/client/plugins" 73 | const authClient = createAuthClient({ 74 | plugins: [oidcClient({ 75 | // Your OIDC configuration 76 | })] 77 | }) 78 | ``` 79 | </Step> 80 | </Steps> 81 | 82 | ## Usage 83 | 84 | Once installed, you can utilize the OIDC Provider to manage authentication flows within your application. 85 | 86 | ### Register a New Client 87 | 88 | To register a new OIDC client, use the `oauth2.register` method. 89 | 90 | 91 | #### Simple Example 92 | 93 | ```ts 94 | const application = await client.oauth2.register({ 95 | client_name: "My Client", 96 | redirect_uris: ["https://client.example.com/callback"], 97 | }); 98 | ``` 99 | 100 | 101 | #### Full Method 102 | 103 | 104 | <APIMethod path="/oauth2/register" method="POST"> 105 | ```ts 106 | type registerOAuthApplication = { 107 | /** 108 | * A list of redirect URIs. 109 | */ 110 | redirect_uris: string[] = ["https://client.example.com/callback"] 111 | /** 112 | * The authentication method for the token endpoint. 113 | */ 114 | token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post" = "client_secret_basic" 115 | /** 116 | * The grant types supported by the application. 117 | */ 118 | grant_types?: ("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[] = ["authorization_code"] 119 | /** 120 | * The response types supported by the application. 121 | */ 122 | response_types?: ("code" | "token")[] = ["code"] 123 | /** 124 | * The name of the application. 125 | */ 126 | client_name?: string = "My App" 127 | /** 128 | * The URI of the application. 129 | */ 130 | client_uri?: string = "https://client.example.com" 131 | /** 132 | * The URI of the application logo. 133 | */ 134 | logo_uri?: string = "https://client.example.com/logo.png" 135 | /** 136 | * The scopes supported by the application. Separated by spaces. 137 | */ 138 | scope?: string = "profile email" 139 | /** 140 | * The contact information for the application. 141 | */ 142 | contacts?: string[] = ["[email protected]"] 143 | /** 144 | * The URI of the application terms of service. 145 | */ 146 | tos_uri?: string = "https://client.example.com/tos" 147 | /** 148 | * The URI of the application privacy policy. 149 | */ 150 | policy_uri?: string = "https://client.example.com/policy" 151 | /** 152 | * The URI of the application JWKS. 153 | */ 154 | jwks_uri?: string = "https://client.example.com/jwks" 155 | /** 156 | * The JWKS of the application. 157 | */ 158 | jwks?: Record<string, any> = {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]} 159 | /** 160 | * The metadata of the application. 161 | */ 162 | metadata?: Record<string, any> = {"key": "value"} 163 | /** 164 | * The software ID of the application. 165 | */ 166 | software_id?: string = "my-software" 167 | /** 168 | * The software version of the application. 169 | */ 170 | software_version?: string = "1.0.0" 171 | /** 172 | * The software statement of the application. 173 | */ 174 | software_statement?: string 175 | } 176 | ``` 177 | </APIMethod> 178 | 179 | <Callout> 180 | This endpoint supports [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591) compliant client registration. 181 | </Callout> 182 | 183 | Once the application is created, you will receive a `client_id` and `client_secret` that you can display to the user. 184 | 185 | ### Trusted Clients 186 | 187 | For first-party applications and internal services, you can configure trusted clients directly in your OIDC provider configuration. Trusted clients bypass database lookups for better performance and can optionally skip consent screens for improved user experience. 188 | 189 | ```ts title="auth.ts" 190 | import { betterAuth } from "better-auth"; 191 | import { oidcProvider } from "better-auth/plugins"; 192 | 193 | const auth = betterAuth({ 194 | plugins: [ 195 | oidcProvider({ 196 | loginPage: "/sign-in", 197 | trustedClients: [ 198 | { 199 | clientId: "internal-dashboard", 200 | clientSecret: "secure-secret-here", 201 | name: "Internal Dashboard", 202 | type: "web", 203 | redirectURLs: ["https://dashboard.company.com/auth/callback"], 204 | disabled: false, 205 | skipConsent: true, // Skip consent for this trusted client 206 | metadata: { internal: true } 207 | }, 208 | { 209 | clientId: "mobile-app", 210 | clientSecret: "mobile-secret", 211 | name: "Company Mobile App", 212 | type: "native", 213 | redirectURLs: ["com.company.app://auth"], 214 | disabled: false, 215 | skipConsent: false, // Still require consent if needed 216 | metadata: {} 217 | } 218 | ] 219 | })] 220 | }) 221 | ``` 222 | 223 | ### UserInfo Endpoint 224 | 225 | The OIDC Provider includes a UserInfo endpoint that allows clients to retrieve information about the authenticated user. This endpoint is available at `/oauth2/userinfo` and requires a valid access token. 226 | 227 | <Endpoint path="/oauth2/userinfo" method="GET" /> 228 | 229 | ```ts title="client-app.ts" 230 | // Example of how a client would use the UserInfo endpoint 231 | const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', { 232 | headers: { 233 | 'Authorization': 'Bearer ACCESS_TOKEN' 234 | } 235 | }); 236 | 237 | const userInfo = await response.json(); 238 | // userInfo contains user details based on the scopes granted 239 | ``` 240 | 241 | The UserInfo endpoint returns different claims based on the scopes that were granted during authorization: 242 | 243 | - With `openid` scope: Returns the user's ID (`sub` claim) 244 | - With `profile` scope: Returns name, picture, given_name, family_name 245 | - With `email` scope: Returns email and email_verified 246 | 247 | The `getAdditionalUserInfoClaim` function receives the user object, requested scopes array, and the client, allowing you to conditionally include claims based on the scopes granted during authorization. These additional claims will be included in both the UserInfo endpoint response and the ID token. 248 | 249 | ### Consent Screen 250 | 251 | When a user is redirected to the OIDC provider for authentication, they may be prompted to authorize the application to access their data. This is known as the consent screen. By default, Better Auth will display a sample consent screen. You can customize the consent screen by providing a `consentPage` option during initialization. 252 | 253 | **Note**: Trusted clients with `skipConsent: true` will bypass the consent screen entirely, providing a seamless experience for first-party applications. 254 | 255 | ```ts title="auth.ts" 256 | import { betterAuth } from "better-auth"; 257 | 258 | export const auth = betterAuth({ 259 | plugins: [oidcProvider({ 260 | consentPage: "/path/to/consent/page" 261 | })] 262 | }) 263 | ``` 264 | 265 | The plugin will redirect the user to the specified path with `consent_code`, `client_id` and `scope` query parameters. You can use this information to display a custom consent screen. Once the user consents, you can call `oauth2.consent` to complete the authorization. 266 | 267 | <Endpoint path="/oauth2/consent" method="POST" /> 268 | 269 | The consent endpoint supports two methods for passing the consent code: 270 | 271 | **Method 1: URL Parameter** 272 | ```ts title="consent-page.ts" 273 | // Get the consent code from the URL 274 | const params = new URLSearchParams(window.location.search); 275 | 276 | // Submit consent with the code in the request body 277 | const consentCode = params.get('consent_code'); 278 | if (!consentCode) { 279 | throw new Error('Consent code not found in URL parameters'); 280 | } 281 | 282 | const res = await client.oauth2.consent({ 283 | accept: true, // or false to deny 284 | consent_code: consentCode, 285 | }); 286 | ``` 287 | 288 | **Method 2: Cookie-Based** 289 | ```ts title="consent-page.ts" 290 | // The consent code is automatically stored in a signed cookie 291 | // Just submit the consent decision 292 | const res = await client.oauth2.consent({ 293 | accept: true, // or false to deny 294 | // consent_code not needed when using cookie-based flow 295 | }); 296 | ``` 297 | 298 | Both methods are fully supported. The URL parameter method works well with mobile apps and third-party contexts, while the cookie-based method provides a simpler implementation for web applications. 299 | 300 | ### Handling Login 301 | 302 | When a user is redirected to the OIDC provider for authentication, if they are not already logged in, they will be redirected to the login page. You can customize the login page by providing a `loginPage` option during initialization. 303 | 304 | ```ts title="auth.ts" 305 | import { betterAuth } from "better-auth"; 306 | 307 | export const auth = betterAuth({ 308 | plugins: [oidcProvider({ 309 | loginPage: "/sign-in" 310 | })] 311 | }) 312 | ``` 313 | 314 | You don't need to handle anything from your side; when a new session is created, the plugin will handle continuing the authorization flow. 315 | 316 | ## Configuration 317 | 318 | ### OIDC Metadata 319 | 320 | Customize the OIDC metadata by providing a configuration object during initialization. 321 | 322 | ```ts title="auth.ts" 323 | import { betterAuth } from "better-auth"; 324 | import { oidcProvider } from "better-auth/plugins"; 325 | 326 | export const auth = betterAuth({ 327 | plugins: [oidcProvider({ 328 | metadata: { 329 | issuer: "https://your-domain.com", 330 | authorization_endpoint: "/custom/oauth2/authorize", 331 | token_endpoint: "/custom/oauth2/token", 332 | // ...other custom metadata 333 | } 334 | })] 335 | }) 336 | ``` 337 | 338 | ### JWKS Endpoint 339 | 340 | The OIDC Provider plugin can integrate with the JWT plugin to provide asymmetric key signing for ID tokens verifiable at a JWKS endpoint. 341 | 342 | To make your plugin OIDC compliant, you **MUST** disable the `/token` endpoint, the OAuth equivalent is located at `/oauth2/token` instead. 343 | 344 | ```ts title="auth.ts" 345 | import { betterAuth } from "better-auth"; 346 | import { oidcProvider } from "better-auth/plugins"; 347 | import { jwt } from "better-auth/plugins"; 348 | 349 | export const auth = betterAuth({ 350 | disabledPaths: [ 351 | "/token", 352 | ], 353 | plugins: [ 354 | jwt(), // Make sure to add the JWT plugin 355 | oidcProvider({ 356 | useJWTPlugin: true, // Enable JWT plugin integration 357 | loginPage: "/sign-in", 358 | // ... other options 359 | }) 360 | ] 361 | }) 362 | ``` 363 | 364 | <Callout type="info"> 365 | When `useJWTPlugin: false` (default), ID tokens are signed with the application secret. 366 | </Callout> 367 | 368 | ### Dynamic Client Registration 369 | 370 | If you want to allow clients to register dynamically, you can enable this feature by setting the `allowDynamicClientRegistration` option to `true`. 371 | 372 | ```ts title="auth.ts" 373 | const auth = betterAuth({ 374 | plugins: [oidcProvider({ 375 | allowDynamicClientRegistration: true, 376 | })] 377 | }) 378 | ``` 379 | 380 | This will allow clients to register using the `/register` endpoint to be publicly available. 381 | 382 | ## Schema 383 | 384 | The OIDC Provider plugin adds the following tables to the database: 385 | 386 | ### OAuth Application 387 | 388 | Table Name: `oauthApplication` 389 | 390 | <DatabaseTable 391 | fields={[ 392 | { 393 | name: "id", 394 | type: "string", 395 | description: "Database ID of the OAuth client", 396 | isPrimaryKey: true 397 | }, 398 | { 399 | name: "clientId", 400 | type: "string", 401 | description: "Unique identifier for each OAuth client", 402 | isPrimaryKey: true 403 | }, 404 | { 405 | name: "clientSecret", 406 | type: "string", 407 | description: "Secret key for the OAuth client. Optional for public clients using PKCE.", 408 | isOptional: true 409 | }, 410 | { 411 | name: "name", 412 | type: "string", 413 | description: "Name of the OAuth client", 414 | isRequired: true 415 | }, 416 | { 417 | name: "redirectURLs", 418 | type: "string", 419 | description: "Comma-separated list of redirect URLs", 420 | isRequired: true 421 | }, 422 | { 423 | name: "metadata", 424 | type: "string", 425 | description: "Additional metadata for the OAuth client", 426 | isOptional: true 427 | }, 428 | { 429 | name: "type", 430 | type: "string", 431 | description: "Type of OAuth client (e.g., web, mobile)", 432 | isRequired: true 433 | }, 434 | { 435 | name: "disabled", 436 | type: "boolean", 437 | description: "Indicates if the client is disabled", 438 | isRequired: true 439 | }, 440 | { 441 | name: "userId", 442 | type: "string", 443 | description: "ID of the user who owns the client. (optional)", 444 | isOptional: true, 445 | references: { model: "user", field: "id" } 446 | }, 447 | { 448 | name: "createdAt", 449 | type: "Date", 450 | description: "Timestamp of when the OAuth client was created" 451 | }, 452 | { 453 | name: "updatedAt", 454 | type: "Date", 455 | description: "Timestamp of when the OAuth client was last updated" 456 | } 457 | ]} 458 | /> 459 | 460 | ### OAuth Access Token 461 | 462 | Table Name: `oauthAccessToken` 463 | 464 | <DatabaseTable 465 | fields={[ 466 | { 467 | name: "id", 468 | type: "string", 469 | description: "Database ID of the access token", 470 | isPrimaryKey: true 471 | }, 472 | { 473 | name: "accessToken", 474 | type: "string", 475 | description: "Access token issued to the client", 476 | }, 477 | { 478 | name: "refreshToken", 479 | type: "string", 480 | description: "Refresh token issued to the client", 481 | isRequired: true 482 | }, 483 | { 484 | name: "accessTokenExpiresAt", 485 | type: "Date", 486 | description: "Expiration date of the access token", 487 | isRequired: true 488 | }, 489 | { 490 | name: "refreshTokenExpiresAt", 491 | type: "Date", 492 | description: "Expiration date of the refresh token", 493 | isRequired: true 494 | }, 495 | { 496 | name: "clientId", 497 | type: "string", 498 | description: "ID of the OAuth client", 499 | isForeignKey: true, 500 | references: { model: "oauthApplication", field: "clientId" } 501 | }, 502 | { 503 | name: "userId", 504 | type: "string", 505 | description: "ID of the user associated with the token", 506 | isForeignKey: true, 507 | references: { model: "user", field: "id" } 508 | }, 509 | { 510 | name: "scopes", 511 | type: "string", 512 | description: "Comma-separated list of scopes granted", 513 | isRequired: true 514 | }, 515 | { 516 | name: "createdAt", 517 | type: "Date", 518 | description: "Timestamp of when the access token was created" 519 | }, 520 | { 521 | name: "updatedAt", 522 | type: "Date", 523 | description: "Timestamp of when the access token was last updated" 524 | } 525 | ]} 526 | /> 527 | 528 | ### OAuth Consent 529 | 530 | Table Name: `oauthConsent` 531 | 532 | <DatabaseTable 533 | fields={[ 534 | { 535 | name: "id", 536 | type: "string", 537 | description: "Database ID of the consent", 538 | isPrimaryKey: true 539 | }, 540 | { 541 | name: "userId", 542 | type: "string", 543 | description: "ID of the user who gave consent", 544 | isForeignKey: true, 545 | references: { model: "user", field: "id" } 546 | }, 547 | { 548 | name: "clientId", 549 | type: "string", 550 | description: "ID of the OAuth client", 551 | isForeignKey: true, 552 | references: { model: "oauthApplication", field: "clientId" } 553 | }, 554 | { 555 | name: "scopes", 556 | type: "string", 557 | description: "Comma-separated list of scopes consented to", 558 | isRequired: true 559 | }, 560 | { 561 | name: "consentGiven", 562 | type: "boolean", 563 | description: "Indicates if consent was given", 564 | isRequired: true 565 | }, 566 | { 567 | name: "createdAt", 568 | type: "Date", 569 | description: "Timestamp of when the consent was given" 570 | }, 571 | { 572 | name: "updatedAt", 573 | type: "Date", 574 | description: "Timestamp of when the consent was last updated" 575 | } 576 | ]} 577 | /> 578 | 579 | ## Options 580 | 581 | **allowDynamicClientRegistration**: `boolean` - Enable or disable dynamic client registration. 582 | 583 | **metadata**: `OIDCMetadata` - Customize the OIDC provider metadata. 584 | 585 | **loginPage**: `string` - Path to the custom login page. 586 | 587 | **consentPage**: `string` - Path to the custom consent page. 588 | 589 | **trustedClients**: `(Client & { skipConsent?: boolean })[]` - Array of trusted clients that are configured directly in the provider options. These clients bypass database lookups and can optionally skip consent screens. 590 | 591 | **getAdditionalUserInfoClaim**: `(user: User, scopes: string[], client: Client) => Record<string, any>` - Function to get additional user info claims. 592 | 593 | **useJWTPlugin**: `boolean` - When `true`, ID tokens are signed using the JWT plugin's asymmetric keys. When `false` (default), ID tokens are signed with HMAC-SHA256 using the application secret. 594 | 595 | **schema**: `AuthPluginSchema` - Customize the OIDC provider schema. 596 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { DBFieldAttribute } from "@better-auth/core/db"; 2 | import type { User, Session } from "../../types"; 3 | import type { AccessControl, Role } from "../access"; 4 | import type { 5 | Invitation, 6 | Member, 7 | Organization, 8 | OrganizationRole, 9 | Team, 10 | TeamMember, 11 | } from "./schema"; 12 | import type { AuthContext } from "@better-auth/core"; 13 | 14 | export interface OrganizationOptions { 15 | /** 16 | * Configure whether new users are able to create new organizations. 17 | * You can also pass a function that returns a boolean. 18 | * 19 | * @example 20 | * ```ts 21 | * allowUserToCreateOrganization: async (user) => { 22 | * const plan = await getUserPlan(user); 23 | * return plan.name === "pro"; 24 | * } 25 | * ``` 26 | * @default true 27 | */ 28 | allowUserToCreateOrganization?: 29 | | boolean 30 | | ((user: User & Record<string, any>) => Promise<boolean> | boolean); 31 | /** 32 | * The maximum number of organizations a user can create. 33 | * 34 | * You can also pass a function that returns a boolean 35 | */ 36 | organizationLimit?: number | ((user: User) => Promise<boolean> | boolean); 37 | /** 38 | * The role that is assigned to the creator of the 39 | * organization. 40 | * 41 | * @default "owner" 42 | */ 43 | creatorRole?: string; 44 | /** 45 | * The maximum number of members allowed in an organization. 46 | * 47 | * @default 100 48 | */ 49 | membershipLimit?: number; 50 | /** 51 | * Configure the roles and permissions for the 52 | * organization plugin. 53 | */ 54 | ac?: AccessControl; 55 | /** 56 | * Custom permissions for roles. 57 | */ 58 | roles?: { 59 | [key in string]?: Role<any>; 60 | }; 61 | /** 62 | * Dynamic access control for the organization plugin. 63 | */ 64 | dynamicAccessControl?: { 65 | /** 66 | * Whether to enable dynamic access control for the organization plugin. 67 | * 68 | * @default false 69 | */ 70 | enabled?: boolean; 71 | /** 72 | * The maximum number of roles that can be created for an organization. 73 | * 74 | * @default Infinite 75 | */ 76 | maximumRolesPerOrganization?: 77 | | number 78 | | ((organizationId: string) => Promise<number> | number); 79 | }; 80 | /** 81 | * Support for team. 82 | */ 83 | teams?: { 84 | /** 85 | * Enable team features. 86 | */ 87 | enabled: boolean; 88 | /** 89 | * Default team configuration 90 | */ 91 | defaultTeam?: { 92 | /** 93 | * Enable creating a default team when an organization is created 94 | * 95 | * @default true 96 | */ 97 | enabled: boolean; 98 | /** 99 | * Pass a custom default team creator function 100 | */ 101 | customCreateDefaultTeam?: ( 102 | organization: Organization & Record<string, any>, 103 | request?: Request, 104 | ) => Promise<Team & Record<string, any>>; 105 | }; 106 | /** 107 | * Maximum number of teams an organization can have. 108 | * 109 | * You can pass a number or a function that returns a number 110 | * 111 | * @default "unlimited" 112 | * 113 | * @param organization 114 | * @param request 115 | * @returns 116 | */ 117 | maximumTeams?: 118 | | (( 119 | data: { 120 | organizationId: string; 121 | session: { 122 | user: User; 123 | session: Session; 124 | } | null; 125 | }, 126 | request?: Request, 127 | ) => number | Promise<number>) 128 | | number; 129 | 130 | /** 131 | * The maximum number of members per team. 132 | * 133 | * if `undefined`, there is no limit. 134 | * 135 | * @default undefined 136 | */ 137 | maximumMembersPerTeam?: 138 | | number 139 | | ((data: { 140 | teamId: string; 141 | session: { user: User; session: Session }; 142 | organizationId: string; 143 | }) => Promise<number> | number) 144 | | undefined; 145 | /** 146 | * By default, if an organization does only have one team, they'll not be able to remove it. 147 | * 148 | * You can disable this behavior by setting this to `false. 149 | * 150 | * @default false 151 | */ 152 | allowRemovingAllTeams?: boolean; 153 | }; 154 | /** 155 | * The expiration time for the invitation link. 156 | * 157 | * @default 48 hours 158 | */ 159 | invitationExpiresIn?: number; 160 | /** 161 | * The maximum invitation a user can send. 162 | * 163 | * @default 100 164 | */ 165 | invitationLimit?: 166 | | number 167 | | (( 168 | data: { 169 | user: User; 170 | organization: Organization; 171 | member: Member; 172 | }, 173 | ctx: AuthContext, 174 | ) => Promise<number> | number); 175 | /** 176 | * Cancel pending invitations on re-invite. 177 | * 178 | * @default false 179 | */ 180 | cancelPendingInvitationsOnReInvite?: boolean; 181 | /** 182 | * Require email verification on accepting or rejecting an invitation 183 | * 184 | * @default false 185 | */ 186 | requireEmailVerificationOnInvitation?: boolean; 187 | /** 188 | * Send an email with the 189 | * invitation link to the user. 190 | * 191 | * Note: Better Auth doesn't 192 | * generate invitation URLs. 193 | * You'll need to construct the 194 | * URL using the invitation ID 195 | * and pass it to the 196 | * acceptInvitation endpoint for 197 | * the user to accept the 198 | * invitation. 199 | * 200 | * @example 201 | * ```ts 202 | * sendInvitationEmail: async (data) => { 203 | * const url = `https://yourapp.com/organization/ 204 | * accept-invitation?id=${data.id}`; 205 | * await sendEmail(data.email, "Invitation to join 206 | * organization", `Click the link to join the 207 | * organization: ${url}`); 208 | * } 209 | * ``` 210 | */ 211 | sendInvitationEmail?: ( 212 | data: { 213 | /** 214 | * the invitation id 215 | */ 216 | id: string; 217 | /** 218 | * the role of the user 219 | */ 220 | role: string; 221 | /** 222 | * the email of the user 223 | */ 224 | email: string; 225 | /** 226 | * the organization the user is invited to join 227 | */ 228 | organization: Organization; 229 | /** 230 | * the invitation object 231 | */ 232 | invitation: Invitation; 233 | /** 234 | * the member who is inviting the user 235 | */ 236 | inviter: Member & { 237 | user: User; 238 | }; 239 | }, 240 | /** 241 | * The request object 242 | */ 243 | request?: Request, 244 | ) => Promise<void>; 245 | /** 246 | * The schema for the organization plugin. 247 | */ 248 | schema?: { 249 | session?: { 250 | fields?: { 251 | activeOrganizationId?: string; 252 | activeTeamId?: string; 253 | }; 254 | }; 255 | organization?: { 256 | modelName?: string; 257 | fields?: { 258 | [key in keyof Omit<Organization, "id">]?: string; 259 | }; 260 | additionalFields?: { 261 | [key in string]: DBFieldAttribute; 262 | }; 263 | }; 264 | member?: { 265 | modelName?: string; 266 | fields?: { 267 | [key in keyof Omit<Member, "id">]?: string; 268 | }; 269 | additionalFields?: { 270 | [key in string]: DBFieldAttribute; 271 | }; 272 | }; 273 | invitation?: { 274 | modelName?: string; 275 | fields?: { 276 | [key in keyof Omit<Invitation, "id">]?: string; 277 | }; 278 | additionalFields?: { 279 | [key in string]: DBFieldAttribute; 280 | }; 281 | }; 282 | team?: { 283 | modelName?: string; 284 | fields?: { 285 | [key in keyof Omit<Team, "id">]?: string; 286 | }; 287 | additionalFields?: { 288 | [key in string]: DBFieldAttribute; 289 | }; 290 | }; 291 | teamMember?: { 292 | modelName?: string; 293 | fields?: { 294 | [key in keyof Omit<TeamMember, "id">]?: string; 295 | }; 296 | }; 297 | organizationRole?: { 298 | modelName?: string; 299 | fields?: { 300 | [key in keyof Omit<OrganizationRole, "id">]?: string; 301 | }; 302 | additionalFields?: { 303 | [key in string]: DBFieldAttribute; 304 | }; 305 | }; 306 | }; 307 | /** 308 | * Disable organization deletion 309 | * 310 | * @default false 311 | */ 312 | disableOrganizationDeletion?: boolean; 313 | /** 314 | * Configure how organization deletion is handled 315 | * 316 | * @deprecated Use `organizationHooks` instead 317 | */ 318 | organizationDeletion?: { 319 | /** 320 | * disable deleting organization 321 | * 322 | * @deprecated Use `disableOrganizationDeletion` instead 323 | */ 324 | disabled?: boolean; 325 | /** 326 | * A callback that runs before the organization is 327 | * deleted 328 | * 329 | * @deprecated Use `organizationHooks` instead 330 | * @param data - organization and user object 331 | * @param request - the request object 332 | * @returns 333 | */ 334 | beforeDelete?: ( 335 | data: { 336 | organization: Organization; 337 | user: User; 338 | }, 339 | request?: Request, 340 | ) => Promise<void>; 341 | /** 342 | * A callback that runs after the organization is 343 | * deleted 344 | * 345 | * @deprecated Use `organizationHooks` instead 346 | * @param data - organization and user object 347 | * @param request - the request object 348 | * @returns 349 | */ 350 | afterDelete?: ( 351 | data: { 352 | organization: Organization; 353 | user: User; 354 | }, 355 | request?: Request, 356 | ) => Promise<void>; 357 | }; 358 | /** 359 | * @deprecated Use `organizationHooks` instead 360 | */ 361 | organizationCreation?: { 362 | disabled?: boolean; 363 | beforeCreate?: ( 364 | data: { 365 | organization: Omit<Organization, "id"> & Record<string, any>; 366 | user: User & Record<string, any>; 367 | }, 368 | request?: Request, 369 | ) => Promise<void | { 370 | data: Record<string, any>; 371 | }>; 372 | afterCreate?: ( 373 | data: { 374 | organization: Organization & Record<string, any>; 375 | member: Member & Record<string, any>; 376 | user: User & Record<string, any>; 377 | }, 378 | request?: Request, 379 | ) => Promise<void>; 380 | }; 381 | /** 382 | * Hooks for organization 383 | */ 384 | organizationHooks?: { 385 | /** 386 | * A callback that runs before the organization is created 387 | * 388 | * You can return a `data` object to override the default data. 389 | * 390 | * @example 391 | * ```ts 392 | * beforeCreateOrganization: async (data) => { 393 | * return { 394 | * data: { 395 | * ...data.organization, 396 | * }, 397 | * }; 398 | * } 399 | * ``` 400 | * 401 | * You can also throw `new APIError` to stop the organization creation. 402 | * 403 | * @example 404 | * ```ts 405 | * beforeCreateOrganization: async (data) => { 406 | * throw new APIError("BAD_REQUEST", { 407 | * message: "Organization creation is disabled", 408 | * }); 409 | * } 410 | */ 411 | beforeCreateOrganization?: (data: { 412 | organization: { 413 | name?: string; 414 | slug?: string; 415 | logo?: string; 416 | metadata?: Record<string, any>; 417 | [key: string]: any; 418 | }; 419 | user: User & Record<string, any>; 420 | }) => Promise<void | { 421 | data: Record<string, any>; 422 | }>; 423 | /** 424 | * A callback that runs after the organization is created 425 | */ 426 | afterCreateOrganization?: (data: { 427 | organization: Organization & Record<string, any>; 428 | member: Member & Record<string, any>; 429 | user: User & Record<string, any>; 430 | }) => Promise<void>; 431 | /** 432 | * A callback that runs before the organization is updated 433 | * 434 | * You can return a `data` object to override the default data. 435 | * 436 | * @example 437 | * ```ts 438 | * beforeUpdateOrganization: async (data) => { 439 | * return { data: { ...data.organization } }; 440 | * } 441 | */ 442 | beforeUpdateOrganization?: (data: { 443 | organization: { 444 | name?: string; 445 | slug?: string; 446 | logo?: string; 447 | metadata?: Record<string, any>; 448 | [key: string]: any; 449 | }; 450 | user: User & Record<string, any>; 451 | member: Member & Record<string, any>; 452 | }) => Promise<void | { 453 | data: { 454 | name?: string; 455 | slug?: string; 456 | logo?: string; 457 | metadata?: Record<string, any>; 458 | [key: string]: any; 459 | }; 460 | }>; 461 | /** 462 | * A callback that runs after the organization is updated 463 | * 464 | * @example 465 | * ```ts 466 | * afterUpdateOrganization: async (data) => { 467 | * console.log(data.organization); 468 | * } 469 | * ``` 470 | */ 471 | afterUpdateOrganization?: (data: { 472 | /** 473 | * Updated organization object 474 | * 475 | * This could be `null` if an adapter doesn't return updated organization. 476 | */ 477 | organization: (Organization & Record<string, any>) | null; 478 | user: User & Record<string, any>; 479 | member: Member & Record<string, any>; 480 | }) => Promise<void>; 481 | /** 482 | * A callback that runs before the organization is deleted 483 | */ 484 | beforeDeleteOrganization?: (data: { 485 | organization: Organization & Record<string, any>; 486 | user: User & Record<string, any>; 487 | }) => Promise<void>; 488 | /** 489 | * A callback that runs after the organization is deleted 490 | */ 491 | afterDeleteOrganization?: (data: { 492 | organization: Organization & Record<string, any>; 493 | user: User & Record<string, any>; 494 | }) => Promise<void>; 495 | /** 496 | * Member hooks 497 | */ 498 | 499 | /** 500 | * A callback that runs before a member is added to an organization 501 | * 502 | * You can return a `data` object to override the default data. 503 | * 504 | * @example 505 | * ```ts 506 | * beforeAddMember: async (data) => { 507 | * return { 508 | * data: { 509 | * ...data.member, 510 | * role: "custom-role" 511 | * } 512 | * }; 513 | * } 514 | * ``` 515 | */ 516 | beforeAddMember?: (data: { 517 | member: { 518 | userId: string; 519 | organizationId: string; 520 | role: string; 521 | [key: string]: any; 522 | }; 523 | user: User & Record<string, any>; 524 | organization: Organization & Record<string, any>; 525 | }) => Promise<void | { 526 | data: Record<string, any>; 527 | }>; 528 | 529 | /** 530 | * A callback that runs after a member is added to an organization 531 | */ 532 | afterAddMember?: (data: { 533 | member: Member & Record<string, any>; 534 | user: User & Record<string, any>; 535 | organization: Organization & Record<string, any>; 536 | }) => Promise<void>; 537 | 538 | /** 539 | * A callback that runs before a member is removed from an organization 540 | */ 541 | beforeRemoveMember?: (data: { 542 | member: Member & Record<string, any>; 543 | user: User & Record<string, any>; 544 | organization: Organization & Record<string, any>; 545 | }) => Promise<void>; 546 | 547 | /** 548 | * A callback that runs after a member is removed from an organization 549 | */ 550 | afterRemoveMember?: (data: { 551 | member: Member & Record<string, any>; 552 | user: User & Record<string, any>; 553 | organization: Organization & Record<string, any>; 554 | }) => Promise<void>; 555 | 556 | /** 557 | * A callback that runs before a member's role is updated 558 | * 559 | * You can return a `data` object to override the default data. 560 | */ 561 | beforeUpdateMemberRole?: (data: { 562 | member: Member & Record<string, any>; 563 | newRole: string; 564 | user: User & Record<string, any>; 565 | organization: Organization & Record<string, any>; 566 | }) => Promise<void | { 567 | data: { 568 | role: string; 569 | [key: string]: any; 570 | }; 571 | }>; 572 | 573 | /** 574 | * A callback that runs after a member's role is updated 575 | */ 576 | afterUpdateMemberRole?: (data: { 577 | member: Member & Record<string, any>; 578 | previousRole: string; 579 | user: User & Record<string, any>; 580 | organization: Organization & Record<string, any>; 581 | }) => Promise<void>; 582 | 583 | /** 584 | * Invitation hooks 585 | */ 586 | 587 | /** 588 | * A callback that runs before an invitation is created 589 | * 590 | * You can return a `data` object to override the default data. 591 | * 592 | * @example 593 | * ```ts 594 | * beforeCreateInvitation: async (data) => { 595 | * return { 596 | * data: { 597 | * ...data.invitation, 598 | * expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 days 599 | * } 600 | * }; 601 | * } 602 | * ``` 603 | */ 604 | beforeCreateInvitation?: (data: { 605 | invitation: { 606 | email: string; 607 | role: string; 608 | organizationId: string; 609 | inviterId: string; 610 | teamId?: string; 611 | [key: string]: any; 612 | }; 613 | inviter: User & Record<string, any>; 614 | organization: Organization & Record<string, any>; 615 | }) => Promise<void | { 616 | data: Record<string, any>; 617 | }>; 618 | 619 | /** 620 | * A callback that runs after an invitation is created 621 | */ 622 | afterCreateInvitation?: (data: { 623 | invitation: Invitation & Record<string, any>; 624 | inviter: User & Record<string, any>; 625 | organization: Organization & Record<string, any>; 626 | }) => Promise<void>; 627 | 628 | /** 629 | * A callback that runs before an invitation is accepted 630 | */ 631 | beforeAcceptInvitation?: (data: { 632 | invitation: Invitation & Record<string, any>; 633 | user: User & Record<string, any>; 634 | organization: Organization & Record<string, any>; 635 | }) => Promise<void>; 636 | 637 | /** 638 | * A callback that runs after an invitation is accepted 639 | */ 640 | afterAcceptInvitation?: (data: { 641 | invitation: Invitation & Record<string, any>; 642 | member: Member & Record<string, any>; 643 | user: User & Record<string, any>; 644 | organization: Organization & Record<string, any>; 645 | }) => Promise<void>; 646 | 647 | /** 648 | * A callback that runs before an invitation is rejected 649 | */ 650 | beforeRejectInvitation?: (data: { 651 | invitation: Invitation & Record<string, any>; 652 | user: User & Record<string, any>; 653 | organization: Organization & Record<string, any>; 654 | }) => Promise<void>; 655 | 656 | /** 657 | * A callback that runs after an invitation is rejected 658 | */ 659 | afterRejectInvitation?: (data: { 660 | invitation: Invitation & Record<string, any>; 661 | user: User & Record<string, any>; 662 | organization: Organization & Record<string, any>; 663 | }) => Promise<void>; 664 | 665 | /** 666 | * A callback that runs before an invitation is cancelled 667 | */ 668 | beforeCancelInvitation?: (data: { 669 | invitation: Invitation & Record<string, any>; 670 | cancelledBy: User & Record<string, any>; 671 | organization: Organization & Record<string, any>; 672 | }) => Promise<void>; 673 | 674 | /** 675 | * A callback that runs after an invitation is cancelled 676 | */ 677 | afterCancelInvitation?: (data: { 678 | invitation: Invitation & Record<string, any>; 679 | cancelledBy: User & Record<string, any>; 680 | organization: Organization & Record<string, any>; 681 | }) => Promise<void>; 682 | 683 | /** 684 | * Team hooks (when teams are enabled) 685 | */ 686 | 687 | /** 688 | * A callback that runs before a team is created 689 | * 690 | * You can return a `data` object to override the default data. 691 | */ 692 | beforeCreateTeam?: (data: { 693 | team: { 694 | name: string; 695 | organizationId: string; 696 | [key: string]: any; 697 | }; 698 | user?: User & Record<string, any>; 699 | organization: Organization & Record<string, any>; 700 | }) => Promise<void | { 701 | data: Record<string, any>; 702 | }>; 703 | 704 | /** 705 | * A callback that runs after a team is created 706 | */ 707 | afterCreateTeam?: (data: { 708 | team: Team & Record<string, any>; 709 | user?: User & Record<string, any>; 710 | organization: Organization & Record<string, any>; 711 | }) => Promise<void>; 712 | 713 | /** 714 | * A callback that runs before a team is updated 715 | * 716 | * You can return a `data` object to override the default data. 717 | */ 718 | beforeUpdateTeam?: (data: { 719 | team: Team & Record<string, any>; 720 | updates: { 721 | name?: string; 722 | [key: string]: any; 723 | }; 724 | user: User & Record<string, any>; 725 | organization: Organization & Record<string, any>; 726 | }) => Promise<void | { 727 | data: Record<string, any>; 728 | }>; 729 | 730 | /** 731 | * A callback that runs after a team is updated 732 | */ 733 | afterUpdateTeam?: (data: { 734 | team: (Team & Record<string, any>) | null; 735 | user: User & Record<string, any>; 736 | organization: Organization & Record<string, any>; 737 | }) => Promise<void>; 738 | 739 | /** 740 | * A callback that runs before a team is deleted 741 | */ 742 | beforeDeleteTeam?: (data: { 743 | team: Team & Record<string, any>; 744 | user?: User & Record<string, any>; 745 | organization: Organization & Record<string, any>; 746 | }) => Promise<void>; 747 | 748 | /** 749 | * A callback that runs after a team is deleted 750 | */ 751 | afterDeleteTeam?: (data: { 752 | team: Team & Record<string, any>; 753 | user?: User & Record<string, any>; 754 | organization: Organization & Record<string, any>; 755 | }) => Promise<void>; 756 | 757 | /** 758 | * A callback that runs before a member is added to a team 759 | */ 760 | beforeAddTeamMember?: (data: { 761 | teamMember: { 762 | teamId: string; 763 | userId: string; 764 | [key: string]: any; 765 | }; 766 | team: Team & Record<string, any>; 767 | user: User & Record<string, any>; 768 | organization: Organization & Record<string, any>; 769 | }) => Promise<void | { 770 | data: Record<string, any>; 771 | }>; 772 | 773 | /** 774 | * A callback that runs after a member is added to a team 775 | */ 776 | afterAddTeamMember?: (data: { 777 | teamMember: TeamMember & Record<string, any>; 778 | team: Team & Record<string, any>; 779 | user: User & Record<string, any>; 780 | organization: Organization & Record<string, any>; 781 | }) => Promise<void>; 782 | 783 | /** 784 | * A callback that runs before a member is removed from a team 785 | */ 786 | beforeRemoveTeamMember?: (data: { 787 | teamMember: TeamMember & Record<string, any>; 788 | team: Team & Record<string, any>; 789 | user: User & Record<string, any>; 790 | organization: Organization & Record<string, any>; 791 | }) => Promise<void>; 792 | 793 | /** 794 | * A callback that runs after a member is removed from a team 795 | */ 796 | afterRemoveTeamMember?: (data: { 797 | teamMember: TeamMember & Record<string, any>; 798 | team: Team & Record<string, any>; 799 | user: User & Record<string, any>; 800 | organization: Organization & Record<string, any>; 801 | }) => Promise<void>; 802 | }; 803 | } 804 | ``` -------------------------------------------------------------------------------- /docs/components/landing/hero.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useEffect, useId, useState } from "react"; 4 | import useMeasure from "react-use-measure"; 5 | import Link from "next/link"; 6 | import clsx from "clsx"; 7 | import { Button } from "@/components/ui/button"; 8 | import { Check, Copy } from "lucide-react"; 9 | import { useTheme } from "next-themes"; 10 | import { Highlight, themes } from "prism-react-renderer"; 11 | import { AnimatePresence, motion, MotionConfig } from "framer-motion"; 12 | import { Builder } from "../builder"; 13 | import { Spotlight } from "./spotlight"; 14 | import { GradientBG } from "./gradient-bg"; 15 | const tabs: { name: "auth.ts" | "client.ts"; code: string }[] = [ 16 | { 17 | name: "auth.ts", 18 | code: `export const auth = betterAuth({ 19 | database: new Pool({ 20 | connectionString: DATABASE_URL, 21 | }), 22 | emailAndPassword: { 23 | enabled: true, 24 | }, 25 | plugins: [ 26 | organization(), 27 | twoFactor(), 28 | ] 29 | })`, 30 | }, 31 | { 32 | name: "client.ts", 33 | code: `const client = createAuthClient({ 34 | plugins: [passkeyClient()] 35 | }); 36 | `, 37 | }, 38 | ]; 39 | 40 | function TrafficLightsIcon(props: React.ComponentPropsWithoutRef<"svg">) { 41 | return ( 42 | <svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}> 43 | <circle cx="5" cy="5" r="4.5" /> 44 | <circle cx="21" cy="5" r="4.5" /> 45 | <circle cx="37" cy="5" r="4.5" /> 46 | </svg> 47 | ); 48 | } 49 | 50 | export default function Hero() { 51 | return ( 52 | <section className="max-h-[40rem] relative w-full flex md:items-center md:justify-center dark:bg-black/[0.96] antialiased bg-grid-white/[0.02] overflow-hidden md:min-h-[40rem]"> 53 | <Spotlight /> 54 | <div className="overflow-hidden px-2 bg-transparent dark:-mb-32 dark:mt-[-4.75rem] dark:pb-32 dark:pt-[4.75rem] md:w-10/12 mx-auto"> 55 | <div className="mx-auto grid lg:max-w-8xl xl:max-w-full grid-cols-1 items-center gap-x-8 gap-y-16 px-4 py-2 lg:grid-cols-2 lg:px-8 lg:py-4 xl:gap-x-16 xl:px-0"> 56 | <div className="relative z-10 text-left mt-0 sm:mt-2 md:mt-8 lg:mt-0 md:text-center lg:text-left"> 57 | <div className="relative"> 58 | <div className="flex flex-col items-center lg:items-start gap-2"> 59 | <div className="flex items-end gap-1 mt-2 "> 60 | <div className="flex items-center gap-1"> 61 | <svg 62 | xmlns="http://www.w3.org/2000/svg" 63 | width="0.8em" 64 | height="0.8em" 65 | viewBox="0 0 24 24" 66 | > 67 | <path 68 | fill="currentColor" 69 | d="M13 4V2c4.66.5 8.33 4.19 8.85 8.85c.6 5.49-3.35 10.43-8.85 11.03v-2c3.64-.45 6.5-3.32 6.96-6.96A7.994 7.994 0 0 0 13 4m-7.33.2A9.8 9.8 0 0 1 11 2v2.06c-1.43.2-2.78.78-3.9 1.68zM2.05 11a9.8 9.8 0 0 1 2.21-5.33L5.69 7.1A8 8 0 0 0 4.05 11zm2.22 7.33A10.04 10.04 0 0 1 2.06 13h2c.18 1.42.75 2.77 1.63 3.9zm1.4 1.41l1.39-1.37h.04c1.13.88 2.48 1.45 3.9 1.63v2c-1.96-.21-3.82-1-5.33-2.26M12 17l1.56-3.42L17 12l-3.44-1.56L12 7l-1.57 3.44L7 12l3.43 1.58z" 70 | ></path> 71 | </svg> 72 | <span className="text-xs text-opacity-75"> 73 | Own Your Auth 74 | </span> 75 | </div> 76 | </div> 77 | </div> 78 | 79 | <p className="text-zinc-800 dark:text-zinc-300 mt-3 tracking-tight text-2xl md:text-3xl"> 80 | The most comprehensive authentication framework for TypeScript. 81 | </p> 82 | <div className="relative mt-2 md:flex items-center gap-2 w-10/12 hidden border border-white/5"> 83 | <GradientBG className="w-full flex items-center justify-between"> 84 | <div className="w-full flex items-center gap-2"> 85 | <p className="md:text-sm text-xs font-mono select-none"> 86 | <span> 87 | <span className="text-[#4498c8]">git:</span> 88 | <span className="text-[#F07178]">(main) </span> 89 | </span> 90 | <span className="italic text-amber-600"> x</span> 91 | </p> 92 | <p className=" relative inline tracking-tight opacity-90 md:text-sm text-xs dark:text-white font-mono text-black"> 93 | npm add{" "} 94 | <span className="relative dark:text-fuchsia-100 text-fuchsia-950"> 95 | better-auth 96 | <span className="absolute h-2 bg-gradient-to-tr from-white via-stone-200 to-stone-300 blur-3xl w-full top-0 left-2"></span> 97 | </span> 98 | </p> 99 | </div> 100 | <div className="flex gap-2 items-center"> 101 | <Link 102 | href="https://www.npmjs.com/package/better-auth" 103 | target="_blank" 104 | > 105 | <svg 106 | xmlns="http://www.w3.org/2000/svg" 107 | width="1em" 108 | height="1em" 109 | viewBox="0 0 128 128" 110 | > 111 | <path 112 | fill="#cb3837" 113 | d="M0 7.062C0 3.225 3.225 0 7.062 0h113.88c3.838 0 7.063 3.225 7.063 7.062v113.88c0 3.838-3.225 7.063-7.063 7.063H7.062c-3.837 0-7.062-3.225-7.062-7.063zm23.69 97.518h40.395l.05-58.532h19.494l-.05 58.581h19.543l.05-78.075l-78.075-.1l-.1 78.126z" 114 | ></path> 115 | <path 116 | fill="#fff" 117 | d="M25.105 65.52V26.512H40.96c8.72 0 26.274.034 39.008.075l23.153.075v77.866H83.645v-58.54H64.057v58.54H25.105z" 118 | ></path> 119 | </svg> 120 | </Link> 121 | <Link 122 | href="https://github.com/better-auth/better-auth" 123 | target="_blank" 124 | > 125 | <svg 126 | xmlns="http://www.w3.org/2000/svg" 127 | width="1em" 128 | height="1em" 129 | viewBox="0 0 256 256" 130 | > 131 | <g fill="none"> 132 | <rect 133 | width="256" 134 | height="256" 135 | fill="#242938" 136 | rx="60" 137 | ></rect> 138 | <path 139 | fill="#fff" 140 | d="M128.001 30C72.779 30 28 74.77 28 130.001c0 44.183 28.653 81.667 68.387 94.89c4.997.926 6.832-2.169 6.832-4.81c0-2.385-.093-10.262-.136-18.618c-27.82 6.049-33.69-11.799-33.69-11.799c-4.55-11.559-11.104-14.632-11.104-14.632c-9.073-6.207.684-6.079.684-6.079c10.042.705 15.33 10.305 15.33 10.305c8.919 15.288 23.394 10.868 29.1 8.313c.898-6.464 3.489-10.875 6.349-13.372c-22.211-2.529-45.56-11.104-45.56-49.421c0-10.918 3.906-19.839 10.303-26.842c-1.039-2.519-4.462-12.69.968-26.464c0 0 8.398-2.687 27.508 10.25c7.977-2.215 16.531-3.326 25.03-3.364c8.498.038 17.06 1.149 25.051 3.365c19.087-12.939 27.473-10.25 27.473-10.25c5.443 13.773 2.019 23.945.98 26.463c6.412 7.003 10.292 15.924 10.292 26.842c0 38.409-23.394 46.866-45.662 49.341c3.587 3.104 6.783 9.189 6.783 18.519c0 13.38-.116 24.149-.116 27.443c0 2.661 1.8 5.779 6.869 4.797C199.383 211.64 228 174.169 228 130.001C228 74.771 183.227 30 128.001 30M65.454 172.453c-.22.497-1.002.646-1.714.305c-.726-.326-1.133-1.004-.898-1.502c.215-.512.999-.654 1.722-.311c.727.326 1.141 1.01.89 1.508m4.919 4.389c-.477.443-1.41.237-2.042-.462c-.654-.697-.777-1.629-.293-2.078c.491-.442 1.396-.235 2.051.462c.654.706.782 1.631.284 2.078m3.374 5.616c-.613.426-1.615.027-2.234-.863c-.613-.889-.613-1.955.013-2.383c.621-.427 1.608-.043 2.236.84c.611.904.611 1.971-.015 2.406m5.707 6.504c-.548.604-1.715.442-2.57-.383c-.874-.806-1.118-1.95-.568-2.555c.555-.606 1.729-.435 2.59.383c.868.804 1.133 1.957.548 2.555m7.376 2.195c-.242.784-1.366 1.14-2.499.807c-1.13-.343-1.871-1.26-1.642-2.052c.235-.788 1.364-1.159 2.505-.803c1.13.341 1.871 1.252 1.636 2.048m8.394.932c.028.824-.932 1.508-2.121 1.523c-1.196.027-2.163-.641-2.176-1.452c0-.833.939-1.51 2.134-1.53c1.19-.023 2.163.639 2.163 1.459m8.246-.316c.143.804-.683 1.631-1.864 1.851c-1.161.212-2.236-.285-2.383-1.083c-.144-.825.697-1.651 1.856-1.865c1.183-.205 2.241.279 2.391 1.097" 141 | ></path> 142 | </g> 143 | </svg> 144 | </Link> 145 | </div> 146 | </GradientBG> 147 | </div> 148 | 149 | { 150 | <> 151 | <div className="mt-4 flex w-fit flex-col gap-4 font-sans md:flex-row md:justify-center lg:justify-start items-center"> 152 | <Link 153 | href="/docs" 154 | className="hover:shadow-sm dark:border-stone-100 dark:hover:shadow-sm border-2 border-black bg-white px-4 py-1.5 text-sm uppercase text-black shadow-[1px_1px_rgba(0,0,0),2px_2px_rgba(0,0,0),3px_3px_rgba(0,0,0),4px_4px_rgba(0,0,0),5px_5px_0px_0px_rgba(0,0,0)] transition duration-200 md:px-8 dark:shadow-[1px_1px_rgba(255,255,255),2px_2px_rgba(255,255,255),3px_3px_rgba(255,255,255),4px_4px_rgba(255,255,255),5px_5px_0px_0px_rgba(255,255,255)]" 155 | > 156 | Get Started 157 | </Link> 158 | <Builder /> 159 | </div> 160 | </> 161 | } 162 | </div> 163 | </div> 164 | 165 | <div className="relative hidden md:block lg:static xl:pl-10"> 166 | <div className="relative"> 167 | <div className="from-sky-300 via-sky-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-5 blur-lg" /> 168 | <div className="from-stone-300 via-stone-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-5" /> 169 | <CodePreview /> 170 | </div> 171 | </div> 172 | </div> 173 | </div> 174 | </section> 175 | ); 176 | } 177 | 178 | function CodePreview() { 179 | const [currentTab, setCurrentTab] = useState<"auth.ts" | "client.ts">( 180 | "auth.ts", 181 | ); 182 | 183 | const theme = useTheme(); 184 | 185 | const code = tabs.find((tab) => tab.name === currentTab)?.code ?? ""; 186 | const [copyState, setCopyState] = useState(false); 187 | const [ref, { height }] = useMeasure(); 188 | const copyToClipboard = (text: string) => { 189 | navigator.clipboard.writeText(text).then(() => { 190 | setCopyState(true); 191 | setTimeout(() => { 192 | setCopyState(false); 193 | }, 2000); 194 | }); 195 | }; 196 | 197 | const [codeTheme, setCodeTheme] = useState(themes.synthwave84); 198 | 199 | useEffect(() => { 200 | setCodeTheme( 201 | theme.resolvedTheme === "light" ? themes.oneLight : themes.synthwave84, 202 | ); 203 | }, [theme.resolvedTheme]); 204 | 205 | return ( 206 | <AnimatePresence initial={false}> 207 | <MotionConfig transition={{ duration: 0.5, type: "spring", bounce: 0 }}> 208 | <motion.div 209 | animate={{ height: height > 0 ? height : undefined }} 210 | className="from-stone-100 to-stone-200 dark:to-black/90 dark:via-black dark:from-stone-950/90 relative overflow-hidden rounded-sm bg-gradient-to-tr ring-1 ring-white/10 backdrop-blur-lg" 211 | > 212 | <div ref={ref}> 213 | <div className="absolute -top-px left-0 right-0 h-px" /> 214 | <div className="absolute -bottom-px left-11 right-20 h-px" /> 215 | <div className="pl-4 pt-4"> 216 | <TrafficLightsIcon className="stroke-slate-500/30 h-2.5 w-auto" /> 217 | 218 | <div className="mt-4 flex space-x-2 text-xs"> 219 | {tabs.map((tab) => ( 220 | <button 221 | key={tab.name} 222 | onClick={() => setCurrentTab(tab.name)} 223 | className={clsx( 224 | "relative isolate flex h-6 cursor-pointer items-center justify-center rounded-full px-2.5", 225 | currentTab === tab.name 226 | ? "text-stone-300" 227 | : "text-slate-500", 228 | )} 229 | > 230 | {tab.name} 231 | {tab.name === currentTab && ( 232 | <motion.div 233 | layoutId="tab-code-preview" 234 | className="bg-stone-800 absolute inset-0 -z-10 rounded-full" 235 | /> 236 | )} 237 | </button> 238 | ))} 239 | </div> 240 | 241 | <div className="mt-6 flex flex-col items-start px-1 text-sm"> 242 | <div className="absolute top-2 right-4"> 243 | <Button 244 | variant="outline" 245 | size="icon" 246 | className="absolute w-5 border-none bg-transparent h-5 top-2 right-0" 247 | onClick={() => copyToClipboard(code)} 248 | > 249 | {copyState ? ( 250 | <Check className="h-3 w-3" /> 251 | ) : ( 252 | <Copy className="h-3 w-3" /> 253 | )} 254 | <span className="sr-only">Copy code</span> 255 | </Button> 256 | </div> 257 | <motion.div 258 | initial={{ opacity: 0 }} 259 | animate={{ opacity: 1 }} 260 | transition={{ duration: 0.5 }} 261 | key={currentTab} 262 | className="relative flex items-start px-1 text-sm" 263 | > 264 | <div 265 | aria-hidden="true" 266 | className="border-slate-300/5 text-slate-600 select-none border-r pr-4 font-mono" 267 | > 268 | {Array.from({ 269 | length: code.split("\n").length, 270 | }).map((_, index) => ( 271 | <div key={index}> 272 | {(index + 1).toString().padStart(2, "0")} 273 | <br /> 274 | </div> 275 | ))} 276 | </div> 277 | <Highlight 278 | key={theme.resolvedTheme} 279 | code={code} 280 | language={"javascript"} 281 | theme={{ 282 | ...codeTheme, 283 | plain: { 284 | backgroundColor: "transparent", 285 | }, 286 | }} 287 | > 288 | {({ 289 | className, 290 | style, 291 | tokens, 292 | getLineProps, 293 | getTokenProps, 294 | }) => ( 295 | <pre 296 | className={clsx(className, "flex overflow-x-auto pb-6")} 297 | style={style} 298 | > 299 | <code className="px-4"> 300 | {tokens.map((line, lineIndex) => ( 301 | <div key={lineIndex} {...getLineProps({ line })}> 302 | {line.map((token, tokenIndex) => ( 303 | <span 304 | key={tokenIndex} 305 | {...getTokenProps({ token })} 306 | /> 307 | ))} 308 | </div> 309 | ))} 310 | </code> 311 | </pre> 312 | )} 313 | </Highlight> 314 | </motion.div> 315 | <motion.div layout className="self-end"> 316 | <Link 317 | href="https://demo.better-auth.com" 318 | target="_blank" 319 | className="shadow-md border shadow-primary-foreground mb-4 ml-auto mr-4 mt-auto flex cursor-pointer items-center gap-2 px-3 py-1 transition-all ease-in-out hover:opacity-70" 320 | > 321 | <svg 322 | xmlns="http://www.w3.org/2000/svg" 323 | width="1em" 324 | height="1em" 325 | viewBox="0 0 24 24" 326 | > 327 | <path 328 | fill="currentColor" 329 | d="M10 20H8V4h2v2h2v3h2v2h2v2h-2v2h-2v3h-2z" 330 | ></path> 331 | </svg> 332 | <p className="text-sm">Demo</p> 333 | </Link> 334 | </motion.div> 335 | </div> 336 | </div> 337 | </div> 338 | </motion.div> 339 | </MotionConfig> 340 | </AnimatePresence> 341 | ); 342 | } 343 | 344 | export function HeroBackground(props: React.ComponentPropsWithoutRef<"svg">) { 345 | const id = useId(); 346 | return ( 347 | <svg 348 | aria-hidden="true" 349 | viewBox="0 0 668 1069" 350 | width={668} 351 | height={1069} 352 | fill="none" 353 | {...props} 354 | > 355 | <defs> 356 | <clipPath id={`${id}-clip-path`}> 357 | <path 358 | fill="#fff" 359 | transform="rotate(-180 334 534.4)" 360 | d="M0 0h668v1068.8H0z" 361 | /> 362 | </clipPath> 363 | </defs> 364 | <g opacity=".4" clipPath={`url(#${id}-clip-path)`} strokeWidth={4}> 365 | <path 366 | opacity=".3" 367 | d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474" 368 | stroke="#334155" 369 | /> 370 | <path 371 | d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588" 372 | stroke="#334155" 373 | /> 374 | <path 375 | d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175" 376 | stroke="#334155" 377 | /> 378 | <circle 379 | cx="83.5" 380 | cy="384.1" 381 | r="10.438" 382 | transform="rotate(-180 83.5 384.1)" 383 | fill="#1E293B" 384 | stroke="#334155" 385 | /> 386 | <circle 387 | cx="83.5" 388 | cy="200.399" 389 | r="10.438" 390 | transform="rotate(-180 83.5 200.399)" 391 | stroke="#334155" 392 | /> 393 | <circle 394 | cx="83.5" 395 | cy="81.412" 396 | r="10.438" 397 | transform="rotate(-180 83.5 81.412)" 398 | stroke="#334155" 399 | /> 400 | <circle 401 | cx="183.699" 402 | cy="375.75" 403 | r="10.438" 404 | transform="rotate(-180 183.699 375.75)" 405 | fill="#1E293B" 406 | stroke="#334155" 407 | /> 408 | <circle 409 | cx="183.699" 410 | cy="563.625" 411 | r="10.438" 412 | transform="rotate(-180 183.699 563.625)" 413 | fill="#1E293B" 414 | stroke="#334155" 415 | /> 416 | <circle 417 | cx="384.1" 418 | cy="651.3" 419 | r="10.438" 420 | transform="rotate(-180 384.1 651.3)" 421 | fill="#1E293B" 422 | stroke="#334155" 423 | /> 424 | <circle 425 | cx="484.301" 426 | cy="574.062" 427 | r="10.438" 428 | transform="rotate(-180 484.301 574.062)" 429 | fill="#0EA5E9" 430 | fillOpacity=".42" 431 | stroke="#0EA5E9" 432 | /> 433 | <circle 434 | cx="384.1" 435 | cy="749.412" 436 | r="10.438" 437 | transform="rotate(-180 384.1 749.412)" 438 | fill="#1E293B" 439 | stroke="#334155" 440 | /> 441 | <circle 442 | cx="384.1" 443 | cy="1027.05" 444 | r="10.438" 445 | transform="rotate(-180 384.1 1027.05)" 446 | stroke="#334155" 447 | /> 448 | <circle 449 | cx="283.9" 450 | cy="924.763" 451 | r="10.438" 452 | transform="rotate(-180 283.9 924.763)" 453 | stroke="#334155" 454 | /> 455 | <circle 456 | cx="183.699" 457 | cy="870.487" 458 | r="10.438" 459 | transform="rotate(-180 183.699 870.487)" 460 | stroke="#334155" 461 | /> 462 | <circle 463 | cx="283.9" 464 | cy="738.975" 465 | r="10.438" 466 | transform="rotate(-180 283.9 738.975)" 467 | fill="#1E293B" 468 | stroke="#334155" 469 | /> 470 | <circle 471 | cx="83.5" 472 | cy="695.138" 473 | r="10.438" 474 | transform="rotate(-180 83.5 695.138)" 475 | fill="#1E293B" 476 | stroke="#334155" 477 | /> 478 | <circle 479 | cx="83.5" 480 | cy="484.3" 481 | r="10.438" 482 | transform="rotate(-180 83.5 484.3)" 483 | fill="#0EA5E9" 484 | fillOpacity=".42" 485 | stroke="#0EA5E9" 486 | /> 487 | <circle 488 | cx="484.301" 489 | cy="432.112" 490 | r="10.438" 491 | transform="rotate(-180 484.301 432.112)" 492 | fill="#1E293B" 493 | stroke="#334155" 494 | /> 495 | <circle 496 | cx="584.5" 497 | cy="432.112" 498 | r="10.438" 499 | transform="rotate(-180 584.5 432.112)" 500 | fill="#1E293B" 501 | stroke="#334155" 502 | /> 503 | <circle 504 | cx="584.5" 505 | cy="642.95" 506 | r="10.438" 507 | transform="rotate(-180 584.5 642.95)" 508 | fill="#1E293B" 509 | stroke="#334155" 510 | /> 511 | <circle 512 | cx="484.301" 513 | cy="851.699" 514 | r="10.438" 515 | transform="rotate(-180 484.301 851.699)" 516 | stroke="#334155" 517 | /> 518 | <circle 519 | cx="384.1" 520 | cy="256.763" 521 | r="10.438" 522 | transform="rotate(-180 384.1 256.763)" 523 | stroke="#334155" 524 | /> 525 | </g> 526 | </svg> 527 | ); 528 | } 529 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/siwe/siwe.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { siwe } from "./index"; 4 | import { siweClient } from "./client"; 5 | 6 | describe("siwe", async (it) => { 7 | const walletAddress = "0x000000000000000000000000000000000000dEaD"; 8 | const domain = "example.com"; 9 | const chainId = 1; // Ethereum mainnet 10 | 11 | it("should generate a valid nonce for a valid public key", async () => { 12 | const { client } = await getTestInstance( 13 | { 14 | plugins: [ 15 | siwe({ 16 | domain, 17 | async getNonce() { 18 | return "A1b2C3d4E5f6G7h8J"; 19 | }, 20 | async verifyMessage({ message, signature }) { 21 | return ( 22 | signature === "valid_signature" && message === "valid_message" 23 | ); 24 | }, 25 | }), 26 | ], 27 | }, 28 | { 29 | clientOptions: { 30 | plugins: [siweClient()], 31 | }, 32 | }, 33 | ); 34 | const { data } = await client.siwe.nonce({ walletAddress, chainId }); 35 | // to be of type string 36 | expect(typeof data?.nonce).toBe("string"); 37 | // to be 17 alphanumeric characters (96 bits of entropy) 38 | expect(data?.nonce).toMatch(/^[a-zA-Z0-9]{17}$/); 39 | }); 40 | 41 | it("should generate a valid nonce with default chainId", async () => { 42 | const { client } = await getTestInstance( 43 | { 44 | plugins: [ 45 | siwe({ 46 | domain, 47 | async getNonce() { 48 | return "A1b2C3d4E5f6G7h8J"; 49 | }, 50 | async verifyMessage({ message, signature }) { 51 | return ( 52 | signature === "valid_signature" && message === "valid_message" 53 | ); 54 | }, 55 | }), 56 | ], 57 | }, 58 | { 59 | clientOptions: { 60 | plugins: [siweClient()], 61 | }, 62 | }, 63 | ); 64 | // Test without chainId (should default to 1) 65 | const { data } = await client.siwe.nonce({ walletAddress }); 66 | expect(typeof data?.nonce).toBe("string"); 67 | expect(data?.nonce).toMatch(/^[a-zA-Z0-9]{17}$/); 68 | }); 69 | 70 | it("should reject verification if nonce is missing", async () => { 71 | const { client } = await getTestInstance( 72 | { 73 | plugins: [ 74 | siwe({ 75 | domain, 76 | async getNonce() { 77 | return "A1b2C3d4E5f6G7h8J"; 78 | }, 79 | async verifyMessage({ message, signature }) { 80 | return ( 81 | signature === "valid_signature" && message === "valid_message" 82 | ); 83 | }, 84 | }), 85 | ], 86 | }, 87 | { 88 | clientOptions: { 89 | plugins: [siweClient()], 90 | }, 91 | }, 92 | ); 93 | const { error } = await client.siwe.verify({ 94 | message: "valid_message", 95 | signature: "valid_signature", 96 | walletAddress, 97 | chainId, 98 | }); 99 | 100 | expect(error).toBeDefined(); 101 | expect(error?.status).toBe(401); 102 | expect(error?.code).toBe("UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE"); 103 | expect(error?.message).toMatch(/nonce/i); 104 | }); 105 | 106 | it("should reject invalid public key", async () => { 107 | const { client } = await getTestInstance( 108 | { 109 | plugins: [ 110 | siwe({ 111 | domain, 112 | async getNonce() { 113 | return "A1b2C3d4E5f6G7h8J"; 114 | }, 115 | async verifyMessage({ message, signature }) { 116 | return ( 117 | signature === "valid_signature" && message === "valid_message" 118 | ); 119 | }, 120 | }), 121 | ], 122 | }, 123 | { 124 | clientOptions: { 125 | plugins: [siweClient()], 126 | }, 127 | }, 128 | ); 129 | const { error } = await client.siwe.nonce({ walletAddress: "invalid" }); 130 | expect(error).toBeDefined(); 131 | expect(error?.status).toBe(400); 132 | expect(error?.message).toBe("Invalid body parameters"); 133 | }); 134 | 135 | it("should reject verification with invalid signature", async () => { 136 | const { client } = await getTestInstance( 137 | { 138 | plugins: [ 139 | siwe({ 140 | domain, 141 | async getNonce() { 142 | return "A1b2C3d4E5f6G7h8J"; 143 | }, 144 | async verifyMessage({ message, signature }) { 145 | return ( 146 | signature === "valid_signature" && message === "valid_message" 147 | ); 148 | }, 149 | }), 150 | ], 151 | }, 152 | { 153 | clientOptions: { 154 | plugins: [siweClient()], 155 | }, 156 | }, 157 | ); 158 | const { error } = await client.siwe.verify({ 159 | message: "Sign in with Ethereum.", 160 | signature: "invalid_signature", 161 | walletAddress, 162 | }); 163 | expect(error).toBeDefined(); 164 | expect(error?.status).toBe(401); 165 | }); 166 | 167 | it("should reject invalid walletAddress format", async () => { 168 | const { client } = await getTestInstance( 169 | { 170 | plugins: [ 171 | siwe({ 172 | domain, 173 | async getNonce() { 174 | return "A1b2C3d4E5f6G7h8J"; 175 | }, 176 | async verifyMessage({ message, signature }) { 177 | return ( 178 | signature === "valid_signature" && message === "valid_message" 179 | ); 180 | }, 181 | }), 182 | ], 183 | }, 184 | { 185 | clientOptions: { 186 | plugins: [siweClient()], 187 | }, 188 | }, 189 | ); 190 | const { error } = await client.siwe.nonce({ 191 | walletAddress: "not_a_valid_key", 192 | }); 193 | expect(error).toBeDefined(); 194 | expect(error?.status).toBe(400); 195 | }); 196 | 197 | it("should reject invalid message", async () => { 198 | const { client } = await getTestInstance( 199 | { 200 | plugins: [ 201 | siwe({ 202 | domain, 203 | async getNonce() { 204 | return "A1b2C3d4E5f6G7h8J"; 205 | }, 206 | async verifyMessage({ message, signature }) { 207 | return ( 208 | signature === "valid_signature" && message === "valid_message" 209 | ); 210 | }, 211 | }), 212 | ], 213 | }, 214 | { 215 | clientOptions: { 216 | plugins: [siweClient()], 217 | }, 218 | }, 219 | ); 220 | const { error } = await client.siwe.verify({ 221 | message: "invalid_message", 222 | signature: "valid_signature", 223 | walletAddress, 224 | }); 225 | expect(error).toBeDefined(); 226 | expect(error?.status).toBe(401); 227 | }); 228 | 229 | it("should reject verification without email when anonymous is false", async () => { 230 | const { client } = await getTestInstance( 231 | { 232 | plugins: [ 233 | siwe({ 234 | domain, 235 | anonymous: false, 236 | async getNonce() { 237 | return "A1b2C3d4E5f6G7h8J"; 238 | }, 239 | async verifyMessage({ message, signature }) { 240 | return ( 241 | signature === "valid_signature" && message === "valid_message" 242 | ); 243 | }, 244 | }), 245 | ], 246 | }, 247 | { 248 | clientOptions: { 249 | plugins: [siweClient()], 250 | }, 251 | }, 252 | ); 253 | 254 | const { error } = await client.siwe.verify({ 255 | message: "valid_message", 256 | signature: "valid_signature", 257 | walletAddress, 258 | email: undefined, 259 | }); 260 | expect(error).toBeDefined(); 261 | expect(error?.status).toBe(400); 262 | expect(error?.message).toBe("Invalid body parameters"); 263 | }); 264 | 265 | it("should accept verification with email when anonymous is false", async () => { 266 | const { client } = await getTestInstance( 267 | { 268 | plugins: [ 269 | siwe({ 270 | domain, 271 | anonymous: false, 272 | async getNonce() { 273 | return "A1b2C3d4E5f6G7h8J"; 274 | }, 275 | async verifyMessage({ message, signature }) { 276 | return ( 277 | signature === "valid_signature" && message === "valid_message" 278 | ); 279 | }, 280 | }), 281 | ], 282 | }, 283 | { 284 | clientOptions: { 285 | plugins: [siweClient()], 286 | }, 287 | }, 288 | ); 289 | 290 | await client.siwe.nonce({ walletAddress, chainId }); 291 | 292 | const { data, error } = await client.siwe.verify({ 293 | message: "valid_message", 294 | signature: "valid_signature", 295 | walletAddress, 296 | chainId, 297 | email: "[email protected]", 298 | }); 299 | expect(error).toBeNull(); 300 | expect(data?.success).toBe(true); 301 | }); 302 | 303 | it("should reject invalid email format when anonymous is false", async () => { 304 | const { client } = await getTestInstance( 305 | { 306 | plugins: [ 307 | siwe({ 308 | domain, 309 | anonymous: false, 310 | async getNonce() { 311 | return "A1b2C3d4E5f6G7h8J"; 312 | }, 313 | async verifyMessage({ message, signature }) { 314 | return ( 315 | signature === "valid_signature" && message === "valid_message" 316 | ); 317 | }, 318 | }), 319 | ], 320 | }, 321 | { 322 | clientOptions: { 323 | plugins: [siweClient()], 324 | }, 325 | }, 326 | ); 327 | 328 | const { error } = await client.siwe.verify({ 329 | message: "valid_message", 330 | signature: "valid_signature", 331 | walletAddress, 332 | email: "not-an-email", 333 | }); 334 | expect(error).toBeDefined(); 335 | expect(error?.status).toBe(400); 336 | expect(error?.message).toBe("Invalid body parameters"); 337 | }); 338 | 339 | it("should allow verification without email when anonymous is true", async () => { 340 | const { client } = await getTestInstance( 341 | { 342 | plugins: [ 343 | siwe({ 344 | domain, 345 | // anonymous: true by default 346 | async getNonce() { 347 | return "A1b2C3d4E5f6G7h8J"; 348 | }, 349 | async verifyMessage({ message, signature }) { 350 | return ( 351 | signature === "valid_signature" && message === "valid_message" 352 | ); 353 | }, 354 | }), 355 | ], 356 | }, 357 | { 358 | clientOptions: { 359 | plugins: [siweClient()], 360 | }, 361 | }, 362 | ); 363 | 364 | await client.siwe.nonce({ walletAddress, chainId }); 365 | const { data, error } = await client.siwe.verify({ 366 | message: "valid_message", 367 | signature: "valid_signature", 368 | walletAddress, 369 | chainId, 370 | }); 371 | expect(error).toBeNull(); 372 | expect(data?.success).toBe(true); 373 | }); 374 | 375 | it("should not allow nonce reuse", async () => { 376 | const { client } = await getTestInstance( 377 | { 378 | plugins: [ 379 | siwe({ 380 | domain, 381 | async getNonce() { 382 | return "A1b2C3d4E5f6G7h8J"; 383 | }, 384 | async verifyMessage({ message, signature }) { 385 | return ( 386 | signature === "valid_signature" && message === "valid_message" 387 | ); 388 | }, 389 | }), 390 | ], 391 | }, 392 | { 393 | clientOptions: { plugins: [siweClient()] }, 394 | }, 395 | ); 396 | 397 | await client.siwe.nonce({ walletAddress, chainId }); 398 | const first = await client.siwe.verify({ 399 | message: "valid_message", 400 | signature: "valid_signature", 401 | walletAddress, 402 | chainId, 403 | }); 404 | expect(first.error).toBeNull(); 405 | expect(first.data?.success).toBe(true); 406 | 407 | // Try to verify again with the same nonce 408 | const second = await client.siwe.verify({ 409 | message: "valid_message", 410 | signature: "valid_signature", 411 | walletAddress, 412 | chainId, 413 | }); 414 | expect(second.error).toBeDefined(); 415 | expect(second.error?.status).toBe(401); 416 | expect(second.error?.code).toBe("UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE"); 417 | }); 418 | 419 | it("should reject empty string email when anonymous is false", async () => { 420 | const { client } = await getTestInstance( 421 | { 422 | plugins: [ 423 | siwe({ 424 | domain, 425 | anonymous: false, 426 | async getNonce() { 427 | return "A1b2C3d4E5f6G7h8J"; 428 | }, 429 | async verifyMessage({ message, signature }) { 430 | return ( 431 | signature === "valid_signature" && message === "valid_message" 432 | ); 433 | }, 434 | }), 435 | ], 436 | }, 437 | { 438 | clientOptions: { plugins: [siweClient()] }, 439 | }, 440 | ); 441 | 442 | await client.siwe.nonce({ walletAddress, chainId }); 443 | const { error } = await client.siwe.verify({ 444 | message: "valid_message", 445 | signature: "valid_signature", 446 | walletAddress, 447 | chainId, 448 | email: "", 449 | }); 450 | expect(error).toBeDefined(); 451 | expect(error?.status).toBe(400); 452 | expect(error?.message).toBe("Invalid body parameters"); 453 | }); 454 | 455 | it("should store and return the wallet address in checksum format", async () => { 456 | const { client, auth } = await getTestInstance( 457 | { 458 | plugins: [ 459 | siwe({ 460 | domain, 461 | async getNonce() { 462 | return "A1b2C3d4E5f6G7h8J"; 463 | }, 464 | async verifyMessage({ message, signature }) { 465 | return ( 466 | signature === "valid_signature" && message === "valid_message" 467 | ); 468 | }, 469 | }), 470 | ], 471 | }, 472 | { 473 | clientOptions: { plugins: [siweClient()] }, 474 | }, 475 | ); 476 | 477 | // Use lowercase address 478 | await client.siwe.nonce({ 479 | walletAddress: walletAddress.toLowerCase(), 480 | chainId, 481 | }); 482 | const { data } = await client.siwe.verify({ 483 | message: "valid_message", 484 | signature: "valid_signature", 485 | walletAddress: walletAddress.toLowerCase(), 486 | chainId, 487 | }); 488 | expect(data?.success).toBe(true); 489 | 490 | // Fetch wallet address from the adapter 491 | const walletAddresses: any[] = await (await auth.$context).adapter.findMany( 492 | { 493 | model: "walletAddress", 494 | where: [{ field: "address", operator: "eq", value: walletAddress }], 495 | }, 496 | ); 497 | expect(walletAddresses.length).toBe(1); 498 | expect(walletAddresses[0]?.address).toBe(walletAddress); // checksummed 499 | 500 | // Try with uppercase address, should not create a new wallet address entry 501 | await client.siwe.nonce({ 502 | walletAddress: walletAddress.toUpperCase(), 503 | chainId, 504 | }); 505 | const { data: data2, error: error2 } = await client.siwe.verify({ 506 | message: "valid_message", 507 | signature: "valid_signature", 508 | walletAddress: walletAddress.toUpperCase(), 509 | chainId, 510 | }); 511 | expect(data2?.success).toBe(true); // Should succeed with existing address 512 | 513 | const walletAddressesAfter = await (await auth.$context).adapter.findMany({ 514 | model: "walletAddress", 515 | where: [{ field: "address", operator: "eq", value: walletAddress }], 516 | }); 517 | expect(walletAddressesAfter.length).toBe(1); // Still only one wallet address entry 518 | }); 519 | 520 | it("should reject duplicate wallet address entries", async () => { 521 | const { client, auth } = await getTestInstance( 522 | { 523 | plugins: [ 524 | siwe({ 525 | domain, 526 | async getNonce() { 527 | return "A1b2C3d4E5f6G7h8J"; 528 | }, 529 | async verifyMessage({ message, signature }) { 530 | return ( 531 | signature === "valid_signature" && message === "valid_message" 532 | ); 533 | }, 534 | }), 535 | ], 536 | }, 537 | { clientOptions: { plugins: [siweClient()] } }, 538 | ); 539 | 540 | const testAddress = "0x000000000000000000000000000000000000dEaD"; 541 | const testChainId = 1; 542 | 543 | // First user successfully creates account with wallet address 544 | await client.siwe.nonce({ 545 | walletAddress: testAddress, 546 | chainId: testChainId, 547 | }); 548 | const firstUser = await client.siwe.verify({ 549 | message: "valid_message", 550 | signature: "valid_signature", 551 | walletAddress: testAddress, 552 | chainId: testChainId, 553 | }); 554 | expect(firstUser.error).toBeNull(); 555 | expect(firstUser.data?.success).toBe(true); 556 | 557 | // Verify wallet address record was created 558 | const walletAddresses: any[] = await (await auth.$context).adapter.findMany( 559 | { 560 | model: "walletAddress", 561 | where: [ 562 | { field: "address", operator: "eq", value: testAddress }, 563 | { field: "chainId", operator: "eq", value: testChainId }, 564 | ], 565 | }, 566 | ); 567 | expect(walletAddresses.length).toBe(1); 568 | expect(walletAddresses[0]?.address).toBe(testAddress); 569 | expect(walletAddresses[0]?.chainId).toBe(testChainId); 570 | expect(walletAddresses[0]?.isPrimary).toBe(true); 571 | 572 | // Second attempt with same address + chainId should use existing user 573 | await client.siwe.nonce({ 574 | walletAddress: testAddress, 575 | chainId: testChainId, 576 | }); 577 | const secondUser = await client.siwe.verify({ 578 | message: "valid_message", 579 | signature: "valid_signature", 580 | walletAddress: testAddress, 581 | chainId: testChainId, 582 | }); 583 | expect(secondUser.error).toBeNull(); 584 | expect(secondUser.data?.success).toBe(true); 585 | expect(secondUser.data?.user.id).toBe(firstUser.data?.user.id); // Same user ID 586 | 587 | // Verify no duplicate wallet address records were created 588 | const walletAddressesAfter: any[] = await ( 589 | await auth.$context 590 | ).adapter.findMany({ 591 | model: "walletAddress", 592 | where: [ 593 | { field: "address", operator: "eq", value: testAddress }, 594 | { field: "chainId", operator: "eq", value: testChainId }, 595 | ], 596 | }); 597 | expect(walletAddressesAfter.length).toBe(1); // Still only one record 598 | 599 | // Verify total user count (should be only 1 user created) 600 | const allUsers: any[] = await (await auth.$context).adapter.findMany({ 601 | model: "user", 602 | }); 603 | const usersWithTestAddress = allUsers.filter((user) => 604 | walletAddressesAfter.some((wa) => wa.userId === user.id), 605 | ); 606 | expect(usersWithTestAddress.length).toBe(1); // Only one user should have this address 607 | }); 608 | 609 | it("should support custom schema with mergeSchema", async () => { 610 | const { client, auth } = await getTestInstance( 611 | { 612 | logger: { 613 | level: "debug", 614 | }, 615 | plugins: [ 616 | siwe({ 617 | domain, 618 | async getNonce() { 619 | return "A1b2C3d4E5f6G7h8J"; 620 | }, 621 | async verifyMessage({ message, signature }) { 622 | return ( 623 | signature === "valid_signature" && message === "valid_message" 624 | ); 625 | }, 626 | schema: { 627 | walletAddress: { 628 | modelName: "wallet_address", 629 | fields: { 630 | userId: "user_id", 631 | address: "wallet_address", 632 | chainId: "chain_id", 633 | isPrimary: "is_primary", 634 | createdAt: "created_at", 635 | }, 636 | }, 637 | }, 638 | }), 639 | ], 640 | }, 641 | { clientOptions: { plugins: [siweClient()] } }, 642 | ); 643 | 644 | const testAddress = "0x000000000000000000000000000000000000dEaD"; 645 | const testChainId = 1; 646 | 647 | // Create account with custom schema 648 | await client.siwe.nonce({ 649 | walletAddress: testAddress, 650 | chainId: testChainId, 651 | }); 652 | const result = await client.siwe.verify({ 653 | message: "valid_message", 654 | signature: "valid_signature", 655 | walletAddress: testAddress, 656 | chainId: testChainId, 657 | }); 658 | expect(result.error).toBeNull(); 659 | expect(result.data?.success).toBe(true); 660 | const context = await auth.$context; 661 | 662 | const walletAddresses: any[] = await context.adapter.findMany({ 663 | model: "walletAddress", 664 | where: [ 665 | { field: "address", operator: "eq", value: testAddress }, 666 | { field: "chainId", operator: "eq", value: testChainId }, 667 | ], 668 | }); 669 | expect(walletAddresses.length).toBe(1); 670 | expect(walletAddresses[0]?.address).toBe(testAddress); 671 | expect(walletAddresses[0]?.chainId).toBe(testChainId); 672 | expect(walletAddresses[0]?.isPrimary).toBe(true); 673 | expect(walletAddresses[0]?.userId).toBeDefined(); 674 | expect(walletAddresses[0]?.createdAt).toBeDefined(); 675 | }); 676 | 677 | it("should allow same address on different chains for same user", async () => { 678 | const { client, auth } = await getTestInstance( 679 | { 680 | plugins: [ 681 | siwe({ 682 | domain, 683 | async getNonce() { 684 | return "A1b2C3d4E5f6G7h8J"; 685 | }, 686 | async verifyMessage({ message, signature }) { 687 | return ( 688 | signature === "valid_signature" && message === "valid_message" 689 | ); 690 | }, 691 | }), 692 | ], 693 | }, 694 | { clientOptions: { plugins: [siweClient()] } }, 695 | ); 696 | 697 | const testAddress = "0x000000000000000000000000000000000000dEaD"; 698 | const chainId1 = 1; // Ethereum 699 | const chainId2 = 137; // Polygon 700 | 701 | // First authentication on Ethereum 702 | await client.siwe.nonce({ walletAddress: testAddress, chainId: chainId1 }); 703 | const ethereumAuth = await client.siwe.verify({ 704 | message: "valid_message", 705 | signature: "valid_signature", 706 | walletAddress: testAddress, 707 | chainId: chainId1, 708 | }); 709 | expect(ethereumAuth.error).toBeNull(); 710 | expect(ethereumAuth.data?.success).toBe(true); 711 | 712 | // Second authentication on Polygon with same address 713 | await client.siwe.nonce({ walletAddress: testAddress, chainId: chainId2 }); 714 | const polygonAuth = await client.siwe.verify({ 715 | message: "valid_message", 716 | signature: "valid_signature", 717 | walletAddress: testAddress, 718 | chainId: chainId2, 719 | }); 720 | expect(polygonAuth.error).toBeNull(); 721 | expect(polygonAuth.data?.success).toBe(true); 722 | expect(polygonAuth.data?.user.id).toBe(ethereumAuth.data?.user.id); // Same user 723 | 724 | // Verify both wallet address records exist 725 | const allWalletAddresses: any[] = await ( 726 | await auth.$context 727 | ).adapter.findMany({ 728 | model: "walletAddress", 729 | where: [{ field: "address", operator: "eq", value: testAddress }], 730 | }); 731 | expect(allWalletAddresses.length).toBe(2); 732 | 733 | const ethereumRecord = allWalletAddresses.find( 734 | (wa) => wa.chainId === chainId1, 735 | ); 736 | const polygonRecord = allWalletAddresses.find( 737 | (wa) => wa.chainId === chainId2, 738 | ); 739 | 740 | expect(ethereumRecord).toBeDefined(); 741 | expect(polygonRecord).toBeDefined(); 742 | expect(ethereumRecord?.isPrimary).toBe(true); // First address is primary 743 | expect(polygonRecord?.isPrimary).toBe(false); // Second address is not primary 744 | expect(ethereumRecord?.userId).toBe(polygonRecord?.userId); // Same user ID 745 | }); 746 | }); 747 | ```