#
tokens: 49450/50000 3/1093 files (page 52/67)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 52 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── e2e.yml
│       ├── preview.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│   └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│   ├── expo-example
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app.config.ts
│   │   ├── assets
│   │   │   ├── bg-image.jpeg
│   │   │   ├── fonts
│   │   │   │   └── SpaceMono-Regular.ttf
│   │   │   ├── icon.png
│   │   │   └── images
│   │   │       ├── adaptive-icon.png
│   │   │       ├── favicon.png
│   │   │       ├── logo.png
│   │   │       ├── partial-react-logo.png
│   │   │       ├── react-logo.png
│   │   │       ├── [email protected]
│   │   │       ├── [email protected]
│   │   │       └── splash.png
│   │   ├── babel.config.js
│   │   ├── components.json
│   │   ├── expo-env.d.ts
│   │   ├── index.ts
│   │   ├── metro.config.js
│   │   ├── nativewind-env.d.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── api
│   │   │   │   │   └── auth
│   │   │   │   │       └── [...route]+api.ts
│   │   │   │   ├── dashboard.tsx
│   │   │   │   ├── forget-password.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── sign-up.tsx
│   │   │   ├── components
│   │   │   │   ├── icons
│   │   │   │   │   └── google.tsx
│   │   │   │   └── ui
│   │   │   │       ├── avatar.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       └── text.tsx
│   │   │   ├── global.css
│   │   │   └── lib
│   │   │       ├── auth-client.ts
│   │   │       ├── auth.ts
│   │   │       ├── icons
│   │   │       │   ├── iconWithClassName.ts
│   │   │       │   └── X.tsx
│   │   │       └── utils.ts
│   │   ├── tailwind.config.js
│   │   └── tsconfig.json
│   └── nextjs
│       ├── .env.example
│       ├── .gitignore
│       ├── app
│       │   ├── (auth)
│       │   │   ├── forget-password
│       │   │   │   └── page.tsx
│       │   │   ├── reset-password
│       │   │   │   └── page.tsx
│       │   │   ├── sign-in
│       │   │   │   ├── loading.tsx
│       │   │   │   └── page.tsx
│       │   │   └── two-factor
│       │   │       ├── otp
│       │   │       │   └── page.tsx
│       │   │       └── page.tsx
│       │   ├── accept-invitation
│       │   │   └── [id]
│       │   │       ├── invitation-error.tsx
│       │   │       └── page.tsx
│       │   ├── admin
│       │   │   └── page.tsx
│       │   ├── api
│       │   │   └── auth
│       │   │       └── [...all]
│       │   │           └── route.ts
│       │   ├── apps
│       │   │   └── register
│       │   │       └── page.tsx
│       │   ├── client-test
│       │   │   └── page.tsx
│       │   ├── dashboard
│       │   │   ├── change-plan.tsx
│       │   │   ├── client.tsx
│       │   │   ├── organization-card.tsx
│       │   │   ├── page.tsx
│       │   │   ├── upgrade-button.tsx
│       │   │   └── user-card.tsx
│       │   ├── device
│       │   │   ├── approve
│       │   │   │   └── page.tsx
│       │   │   ├── denied
│       │   │   │   └── page.tsx
│       │   │   ├── layout.tsx
│       │   │   ├── page.tsx
│       │   │   └── success
│       │   │       └── page.tsx
│       │   ├── favicon.ico
│       │   ├── features.tsx
│       │   ├── fonts
│       │   │   ├── GeistMonoVF.woff
│       │   │   └── GeistVF.woff
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── oauth
│       │   │   └── authorize
│       │   │       ├── concet-buttons.tsx
│       │   │       └── page.tsx
│       │   ├── page.tsx
│       │   └── pricing
│       │       └── page.tsx
│       ├── components
│       │   ├── account-switch.tsx
│       │   ├── blocks
│       │   │   └── pricing.tsx
│       │   ├── logo.tsx
│       │   ├── one-tap.tsx
│       │   ├── sign-in-btn.tsx
│       │   ├── sign-in.tsx
│       │   ├── sign-up.tsx
│       │   ├── theme-provider.tsx
│       │   ├── theme-toggle.tsx
│       │   ├── tier-labels.tsx
│       │   ├── ui
│       │   │   ├── accordion.tsx
│       │   │   ├── alert-dialog.tsx
│       │   │   ├── alert.tsx
│       │   │   ├── aspect-ratio.tsx
│       │   │   ├── avatar.tsx
│       │   │   ├── badge.tsx
│       │   │   ├── breadcrumb.tsx
│       │   │   ├── button.tsx
│       │   │   ├── calendar.tsx
│       │   │   ├── card.tsx
│       │   │   ├── carousel.tsx
│       │   │   ├── chart.tsx
│       │   │   ├── checkbox.tsx
│       │   │   ├── collapsible.tsx
│       │   │   ├── command.tsx
│       │   │   ├── context-menu.tsx
│       │   │   ├── copy-button.tsx
│       │   │   ├── dialog.tsx
│       │   │   ├── drawer.tsx
│       │   │   ├── dropdown-menu.tsx
│       │   │   ├── form.tsx
│       │   │   ├── hover-card.tsx
│       │   │   ├── input-otp.tsx
│       │   │   ├── input.tsx
│       │   │   ├── label.tsx
│       │   │   ├── menubar.tsx
│       │   │   ├── navigation-menu.tsx
│       │   │   ├── pagination.tsx
│       │   │   ├── password-input.tsx
│       │   │   ├── popover.tsx
│       │   │   ├── progress.tsx
│       │   │   ├── radio-group.tsx
│       │   │   ├── resizable.tsx
│       │   │   ├── scroll-area.tsx
│       │   │   ├── select.tsx
│       │   │   ├── separator.tsx
│       │   │   ├── sheet.tsx
│       │   │   ├── skeleton.tsx
│       │   │   ├── slider.tsx
│       │   │   ├── sonner.tsx
│       │   │   ├── switch.tsx
│       │   │   ├── table.tsx
│       │   │   ├── tabs.tsx
│       │   │   ├── tabs2.tsx
│       │   │   ├── textarea.tsx
│       │   │   ├── toast.tsx
│       │   │   ├── toaster.tsx
│       │   │   ├── toggle-group.tsx
│       │   │   ├── toggle.tsx
│       │   │   └── tooltip.tsx
│       │   └── wrapper.tsx
│       ├── components.json
│       ├── hooks
│       │   └── use-toast.ts
│       ├── lib
│       │   ├── auth-client.ts
│       │   ├── auth-types.ts
│       │   ├── auth.ts
│       │   ├── email
│       │   │   ├── invitation.tsx
│       │   │   ├── resend.ts
│       │   │   └── reset-password.tsx
│       │   ├── metadata.ts
│       │   ├── shared.ts
│       │   └── utils.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── proxy.ts
│       ├── public
│       │   ├── __og.png
│       │   ├── _og.png
│       │   ├── favicon
│       │   │   ├── android-chrome-192x192.png
│       │   │   ├── android-chrome-512x512.png
│       │   │   ├── apple-touch-icon.png
│       │   │   ├── favicon-16x16.png
│       │   │   ├── favicon-32x32.png
│       │   │   ├── favicon.ico
│       │   │   ├── light
│       │   │   │   ├── android-chrome-192x192.png
│       │   │   │   ├── android-chrome-512x512.png
│       │   │   │   ├── apple-touch-icon.png
│       │   │   │   ├── favicon-16x16.png
│       │   │   │   ├── favicon-32x32.png
│       │   │   │   ├── favicon.ico
│       │   │   │   └── site.webmanifest
│       │   │   └── site.webmanifest
│       │   ├── logo.svg
│       │   └── og.png
│       ├── README.md
│       ├── tailwind.config.ts
│       ├── tsconfig.json
│       └── turbo.json
├── docker-compose.yml
├── docs
│   ├── .env.example
│   ├── .gitignore
│   ├── app
│   │   ├── api
│   │   │   ├── ai-chat
│   │   │   │   └── route.ts
│   │   │   ├── analytics
│   │   │   │   ├── conversation
│   │   │   │   │   └── route.ts
│   │   │   │   ├── event
│   │   │   │   │   └── route.ts
│   │   │   │   └── feedback
│   │   │   │       └── route.ts
│   │   │   ├── chat
│   │   │   │   └── route.ts
│   │   │   ├── og
│   │   │   │   └── route.tsx
│   │   │   ├── og-release
│   │   │   │   └── route.tsx
│   │   │   ├── search
│   │   │   │   └── route.ts
│   │   │   └── support
│   │   │       └── route.ts
│   │   ├── blog
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── blog-list.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   ├── stat-field.tsx
│   │   │   │   └── support.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── changelogs
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── grid-pattern.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   └── stat-field.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── community
│   │   │   ├── _components
│   │   │   │   ├── header.tsx
│   │   │   │   └── stats.tsx
│   │   │   └── page.tsx
│   │   ├── docs
│   │   │   ├── [[...slug]]
│   │   │   │   ├── page.client.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   └── lib
│   │   │       └── get-llm-text.ts
│   │   ├── global.css
│   │   ├── layout.config.tsx
│   │   ├── layout.tsx
│   │   ├── llms.txt
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   ├── reference
│   │   │   └── route.ts
│   │   ├── sitemap.xml
│   │   ├── static.json
│   │   │   └── route.ts
│   │   └── v1
│   │       ├── _components
│   │       │   └── v1-text.tsx
│   │       ├── bg-line.tsx
│   │       └── page.tsx
│   ├── assets
│   │   ├── Geist.ttf
│   │   └── GeistMono.ttf
│   ├── components
│   │   ├── ai-chat-modal.tsx
│   │   ├── anchor-scroll-fix.tsx
│   │   ├── api-method-tabs.tsx
│   │   ├── api-method.tsx
│   │   ├── banner.tsx
│   │   ├── blocks
│   │   │   └── features.tsx
│   │   ├── builder
│   │   │   ├── beam.tsx
│   │   │   ├── code-tabs
│   │   │   │   ├── code-editor.tsx
│   │   │   │   ├── code-tabs.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── tab-bar.tsx
│   │   │   │   └── theme.ts
│   │   │   ├── index.tsx
│   │   │   ├── sign-in.tsx
│   │   │   ├── sign-up.tsx
│   │   │   ├── social-provider.tsx
│   │   │   ├── store.ts
│   │   │   └── tabs.tsx
│   │   ├── display-techstack.tsx
│   │   ├── divider-text.tsx
│   │   ├── docs
│   │   │   ├── docs.client.tsx
│   │   │   ├── docs.tsx
│   │   │   ├── layout
│   │   │   │   ├── nav.tsx
│   │   │   │   ├── theme-toggle.tsx
│   │   │   │   ├── toc-thumb.tsx
│   │   │   │   └── toc.tsx
│   │   │   ├── page.client.tsx
│   │   │   ├── page.tsx
│   │   │   ├── shared.tsx
│   │   │   └── ui
│   │   │       ├── button.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── popover.tsx
│   │   │       └── scroll-area.tsx
│   │   ├── endpoint.tsx
│   │   ├── features.tsx
│   │   ├── floating-ai-search.tsx
│   │   ├── fork-button.tsx
│   │   ├── generate-apple-jwt.tsx
│   │   ├── generate-secret.tsx
│   │   ├── github-stat.tsx
│   │   ├── icons.tsx
│   │   ├── landing
│   │   │   ├── gradient-bg.tsx
│   │   │   ├── grid-pattern.tsx
│   │   │   ├── hero.tsx
│   │   │   ├── section-svg.tsx
│   │   │   ├── section.tsx
│   │   │   ├── spotlight.tsx
│   │   │   └── testimonials.tsx
│   │   ├── logo-context-menu.tsx
│   │   ├── logo.tsx
│   │   ├── markdown-renderer.tsx
│   │   ├── markdown.tsx
│   │   ├── mdx
│   │   │   ├── add-to-cursor.tsx
│   │   │   └── database-tables.tsx
│   │   ├── message-feedback.tsx
│   │   ├── mobile-search-icon.tsx
│   │   ├── nav-bar.tsx
│   │   ├── nav-link.tsx
│   │   ├── nav-mobile.tsx
│   │   ├── promo-card.tsx
│   │   ├── resource-card.tsx
│   │   ├── resource-grid.tsx
│   │   ├── resource-section.tsx
│   │   ├── ripple.tsx
│   │   ├── search-dialog.tsx
│   │   ├── side-bar.tsx
│   │   ├── sidebar-content.tsx
│   │   ├── techstack-icons.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggler.tsx
│   │   └── ui
│   │       ├── accordion.tsx
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── aside-link.tsx
│   │       ├── aspect-ratio.tsx
│   │       ├── avatar.tsx
│   │       ├── background-beams.tsx
│   │       ├── background-boxes.tsx
│   │       ├── badge.tsx
│   │       ├── breadcrumb.tsx
│   │       ├── button.tsx
│   │       ├── calendar.tsx
│   │       ├── callout.tsx
│   │       ├── card.tsx
│   │       ├── carousel.tsx
│   │       ├── chart.tsx
│   │       ├── checkbox.tsx
│   │       ├── code-block.tsx
│   │       ├── collapsible.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── dynamic-code-block.tsx
│   │       ├── fade-in.tsx
│   │       ├── form.tsx
│   │       ├── hover-card.tsx
│   │       ├── input-otp.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── menubar.tsx
│   │       ├── navigation-menu.tsx
│   │       ├── pagination.tsx
│   │       ├── popover.tsx
│   │       ├── progress.tsx
│   │       ├── radio-group.tsx
│   │       ├── resizable.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── separator.tsx
│   │       ├── sheet.tsx
│   │       ├── sidebar.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── sonner.tsx
│   │       ├── sparkles.tsx
│   │       ├── switch.tsx
│   │       ├── table.tsx
│   │       ├── tabs.tsx
│   │       ├── textarea.tsx
│   │       ├── toggle-group.tsx
│   │       ├── toggle.tsx
│   │       ├── tooltip-docs.tsx
│   │       ├── tooltip.tsx
│   │       └── use-copy-button.tsx
│   ├── components.json
│   ├── content
│   │   ├── blogs
│   │   │   ├── 0-supabase-auth-to-planetscale-migration.mdx
│   │   │   ├── 1-3.mdx
│   │   │   ├── authjs-joins-better-auth.mdx
│   │   │   └── seed-round.mdx
│   │   ├── changelogs
│   │   │   ├── 1-2.mdx
│   │   │   └── 1.0.mdx
│   │   └── docs
│   │       ├── adapters
│   │       │   ├── community-adapters.mdx
│   │       │   ├── drizzle.mdx
│   │       │   ├── mongo.mdx
│   │       │   ├── mssql.mdx
│   │       │   ├── mysql.mdx
│   │       │   ├── other-relational-databases.mdx
│   │       │   ├── postgresql.mdx
│   │       │   ├── prisma.mdx
│   │       │   └── sqlite.mdx
│   │       ├── authentication
│   │       │   ├── apple.mdx
│   │       │   ├── atlassian.mdx
│   │       │   ├── cognito.mdx
│   │       │   ├── discord.mdx
│   │       │   ├── dropbox.mdx
│   │       │   ├── email-password.mdx
│   │       │   ├── facebook.mdx
│   │       │   ├── figma.mdx
│   │       │   ├── github.mdx
│   │       │   ├── gitlab.mdx
│   │       │   ├── google.mdx
│   │       │   ├── huggingface.mdx
│   │       │   ├── kakao.mdx
│   │       │   ├── kick.mdx
│   │       │   ├── line.mdx
│   │       │   ├── linear.mdx
│   │       │   ├── linkedin.mdx
│   │       │   ├── microsoft.mdx
│   │       │   ├── naver.mdx
│   │       │   ├── notion.mdx
│   │       │   ├── other-social-providers.mdx
│   │       │   ├── paypal.mdx
│   │       │   ├── reddit.mdx
│   │       │   ├── roblox.mdx
│   │       │   ├── salesforce.mdx
│   │       │   ├── slack.mdx
│   │       │   ├── spotify.mdx
│   │       │   ├── tiktok.mdx
│   │       │   ├── twitch.mdx
│   │       │   ├── twitter.mdx
│   │       │   ├── vk.mdx
│   │       │   └── zoom.mdx
│   │       ├── basic-usage.mdx
│   │       ├── comparison.mdx
│   │       ├── concepts
│   │       │   ├── api.mdx
│   │       │   ├── cli.mdx
│   │       │   ├── client.mdx
│   │       │   ├── cookies.mdx
│   │       │   ├── database.mdx
│   │       │   ├── email.mdx
│   │       │   ├── hooks.mdx
│   │       │   ├── oauth.mdx
│   │       │   ├── plugins.mdx
│   │       │   ├── rate-limit.mdx
│   │       │   ├── session-management.mdx
│   │       │   ├── typescript.mdx
│   │       │   └── users-accounts.mdx
│   │       ├── examples
│   │       │   ├── astro.mdx
│   │       │   ├── next-js.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   └── svelte-kit.mdx
│   │       ├── guides
│   │       │   ├── auth0-migration-guide.mdx
│   │       │   ├── browser-extension-guide.mdx
│   │       │   ├── clerk-migration-guide.mdx
│   │       │   ├── create-a-db-adapter.mdx
│   │       │   ├── next-auth-migration-guide.mdx
│   │       │   ├── optimizing-for-performance.mdx
│   │       │   ├── saml-sso-with-okta.mdx
│   │       │   ├── supabase-migration-guide.mdx
│   │       │   └── your-first-plugin.mdx
│   │       ├── installation.mdx
│   │       ├── integrations
│   │       │   ├── astro.mdx
│   │       │   ├── convex.mdx
│   │       │   ├── elysia.mdx
│   │       │   ├── expo.mdx
│   │       │   ├── express.mdx
│   │       │   ├── fastify.mdx
│   │       │   ├── hono.mdx
│   │       │   ├── lynx.mdx
│   │       │   ├── nestjs.mdx
│   │       │   ├── next.mdx
│   │       │   ├── nitro.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   ├── solid-start.mdx
│   │       │   ├── svelte-kit.mdx
│   │       │   ├── tanstack.mdx
│   │       │   └── waku.mdx
│   │       ├── introduction.mdx
│   │       ├── meta.json
│   │       ├── plugins
│   │       │   ├── 2fa.mdx
│   │       │   ├── admin.mdx
│   │       │   ├── anonymous.mdx
│   │       │   ├── api-key.mdx
│   │       │   ├── autumn.mdx
│   │       │   ├── bearer.mdx
│   │       │   ├── captcha.mdx
│   │       │   ├── community-plugins.mdx
│   │       │   ├── device-authorization.mdx
│   │       │   ├── dodopayments.mdx
│   │       │   ├── dub.mdx
│   │       │   ├── email-otp.mdx
│   │       │   ├── generic-oauth.mdx
│   │       │   ├── have-i-been-pwned.mdx
│   │       │   ├── jwt.mdx
│   │       │   ├── last-login-method.mdx
│   │       │   ├── magic-link.mdx
│   │       │   ├── mcp.mdx
│   │       │   ├── multi-session.mdx
│   │       │   ├── oauth-proxy.mdx
│   │       │   ├── oidc-provider.mdx
│   │       │   ├── one-tap.mdx
│   │       │   ├── one-time-token.mdx
│   │       │   ├── open-api.mdx
│   │       │   ├── organization.mdx
│   │       │   ├── passkey.mdx
│   │       │   ├── phone-number.mdx
│   │       │   ├── polar.mdx
│   │       │   ├── siwe.mdx
│   │       │   ├── sso.mdx
│   │       │   ├── stripe.mdx
│   │       │   └── username.mdx
│   │       └── reference
│   │           ├── contributing.mdx
│   │           ├── faq.mdx
│   │           ├── options.mdx
│   │           ├── resources.mdx
│   │           ├── security.mdx
│   │           └── telemetry.mdx
│   ├── hooks
│   │   └── use-mobile.ts
│   ├── ignore-build.sh
│   ├── lib
│   │   ├── blog.ts
│   │   ├── chat
│   │   │   └── inkeep-qa-schema.ts
│   │   ├── constants.ts
│   │   ├── export-search-indexes.ts
│   │   ├── inkeep-analytics.ts
│   │   ├── is-active.ts
│   │   ├── metadata.ts
│   │   ├── source.ts
│   │   └── utils.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── proxy.ts
│   ├── public
│   │   ├── avatars
│   │   │   └── beka.jpg
│   │   ├── blogs
│   │   │   ├── authjs-joins.png
│   │   │   ├── seed-round.png
│   │   │   └── supabase-ps.png
│   │   ├── branding
│   │   │   ├── better-auth-brand-assets.zip
│   │   │   ├── better-auth-logo-dark.png
│   │   │   ├── better-auth-logo-dark.svg
│   │   │   ├── better-auth-logo-light.png
│   │   │   ├── better-auth-logo-light.svg
│   │   │   ├── better-auth-logo-wordmark-dark.png
│   │   │   ├── better-auth-logo-wordmark-dark.svg
│   │   │   ├── better-auth-logo-wordmark-light.png
│   │   │   └── better-auth-logo-wordmark-light.svg
│   │   ├── extension-id.png
│   │   ├── favicon
│   │   │   ├── android-chrome-192x192.png
│   │   │   ├── android-chrome-512x512.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── favicon-16x16.png
│   │   │   ├── favicon-32x32.png
│   │   │   ├── favicon.ico
│   │   │   ├── light
│   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   ├── apple-touch-icon.png
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   └── site.webmanifest
│   │   │   └── site.webmanifest
│   │   ├── images
│   │   │   └── blogs
│   │   │       └── better auth (1).png
│   │   ├── logo.png
│   │   ├── logo.svg
│   │   ├── LogoDark.webp
│   │   ├── LogoLight.webp
│   │   ├── og.png
│   │   ├── open-api-reference.png
│   │   ├── people-say
│   │   │   ├── code-with-antonio.jpg
│   │   │   ├── dagmawi-babi.png
│   │   │   ├── dax.png
│   │   │   ├── dev-ed.png
│   │   │   ├── egoist.png
│   │   │   ├── guillermo-rauch.png
│   │   │   ├── jonathan-wilke.png
│   │   │   ├── josh-tried-coding.jpg
│   │   │   ├── kitze.jpg
│   │   │   ├── lazar-nikolov.png
│   │   │   ├── nizzy.png
│   │   │   ├── omar-mcadam.png
│   │   │   ├── ryan-vogel.jpg
│   │   │   ├── saltyatom.jpg
│   │   │   ├── sebastien-chopin.png
│   │   │   ├── shreyas-mididoddi.png
│   │   │   ├── tech-nerd.png
│   │   │   ├── theo.png
│   │   │   ├── vybhav-bhargav.png
│   │   │   └── xavier-pladevall.jpg
│   │   ├── plus.svg
│   │   ├── release-og
│   │   │   ├── 1-2.png
│   │   │   ├── 1-3.png
│   │   │   └── changelog-og.png
│   │   └── v1-og.png
│   ├── README.md
│   ├── scripts
│   │   ├── endpoint-to-doc
│   │   │   ├── index.ts
│   │   │   ├── input.ts
│   │   │   ├── output.mdx
│   │   │   └── readme.md
│   │   └── sync-orama.ts
│   ├── source.config.ts
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── turbo.json
├── e2e
│   ├── integration
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── solid-vinxi
│   │   │   ├── .gitignore
│   │   │   ├── app.config.ts
│   │   │   ├── e2e
│   │   │   │   ├── test.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── entry-client.tsx
│   │   │   │   ├── entry-server.tsx
│   │   │   │   ├── global.d.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── auth-client.ts
│   │   │   │   │   └── auth.ts
│   │   │   │   └── routes
│   │   │   │       ├── [...404].tsx
│   │   │   │       ├── api
│   │   │   │       │   └── auth
│   │   │   │       │       └── [...all].ts
│   │   │   │       └── index.tsx
│   │   │   └── tsconfig.json
│   │   ├── test-utils
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── playwright.ts
│   │   └── vanilla-node
│   │       ├── e2e
│   │       │   ├── app.ts
│   │       │   ├── domain.spec.ts
│   │       │   ├── postgres-js.spec.ts
│   │       │   ├── test.spec.ts
│   │       │   └── utils.ts
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── src
│   │       │   ├── main.ts
│   │       │   └── vite-env.d.ts
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   └── smoke
│       ├── package.json
│       ├── test
│       │   ├── bun.spec.ts
│       │   ├── cloudflare.spec.ts
│       │   ├── deno.spec.ts
│       │   ├── fixtures
│       │   │   ├── bun-simple.ts
│       │   │   ├── cloudflare
│       │   │   │   ├── .gitignore
│       │   │   │   ├── drizzle
│       │   │   │   │   ├── 0000_clean_vector.sql
│       │   │   │   │   └── meta
│       │   │   │   │       ├── _journal.json
│       │   │   │   │       └── 0000_snapshot.json
│       │   │   │   ├── drizzle.config.ts
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── auth-schema.ts
│       │   │   │   │   ├── db.ts
│       │   │   │   │   └── index.ts
│       │   │   │   ├── test
│       │   │   │   │   ├── apply-migrations.ts
│       │   │   │   │   ├── env.d.ts
│       │   │   │   │   └── index.test.ts
│       │   │   │   ├── tsconfig.json
│       │   │   │   ├── vitest.config.ts
│       │   │   │   ├── worker-configuration.d.ts
│       │   │   │   └── wrangler.json
│       │   │   ├── deno-simple.ts
│       │   │   ├── tsconfig-decelration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── 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
│   │   │   │   ├── 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

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/test.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { expect, test, describe, beforeAll } from "vitest";
   2 | import type { User } from "../types";
   3 | import type { BetterAuthOptions } from "@better-auth/core";
   4 | import type { DBAdapter } from "@better-auth/core/db/adapter";
   5 | import { generateId } from "../utils";
   6 | 
   7 | interface AdapterTestOptions {
   8 | 	getAdapter: (
   9 | 		customOptions?: Omit<BetterAuthOptions, "database">,
  10 | 	) => Promise<DBAdapter<BetterAuthOptions>> | DBAdapter<BetterAuthOptions>;
  11 | 	disableTests?: Partial<Record<keyof typeof adapterTests, boolean>>;
  12 | 	testPrefix?: string;
  13 | }
  14 | 
  15 | interface NumberIdAdapterTestOptions {
  16 | 	getAdapter: (
  17 | 		customOptions?: Omit<BetterAuthOptions, "database">,
  18 | 	) => Promise<DBAdapter<BetterAuthOptions>>;
  19 | 	disableTests?: Partial<Record<keyof typeof numberIdAdapterTests, boolean>>;
  20 | 	testPrefix?: string;
  21 | }
  22 | 
  23 | const adapterTests = {
  24 | 	CREATE_MODEL: "create model",
  25 | 	CREATE_MODEL_SHOULD_ALWAYS_RETURN_AN_ID:
  26 | 		"create model should always return an id",
  27 | 	FIND_MODEL: "find model",
  28 | 	FIND_MODEL_WITHOUT_ID: "find model without id",
  29 | 	FIND_MODEL_WITH_SELECT: "find model with select",
  30 | 	FIND_MODEL_WITH_MODIFIED_FIELD_NAME: "find model with modified field name",
  31 | 	UPDATE_MODEL: "update model",
  32 | 	SHOULD_FIND_MANY: "should find many",
  33 | 	SHOULD_FIND_MANY_WITH_WHERE: "should find many with where",
  34 | 	SHOULD_FIND_MANY_WITH_OPERATORS: "should find many with operators",
  35 | 	SHOULD_WORK_WITH_REFERENCE_FIELDS: "should work with reference fields",
  36 | 	SHOULD_FIND_MANY_WITH_NOT_IN_OPERATOR:
  37 | 		"should find many with not in operator",
  38 | 	SHOULD_FIND_MANY_WITH_SORT_BY: "should find many with sortBy",
  39 | 	SHOULD_FIND_MANY_WITH_LIMIT: "should find many with limit",
  40 | 	SHOULD_FIND_MANY_WITH_OFFSET: "should find many with offset",
  41 | 	SHOULD_UPDATE_WITH_MULTIPLE_WHERE: "should update with multiple where",
  42 | 	DELETE_MODEL: "delete model",
  43 | 	SHOULD_DELETE_MANY: "should delete many",
  44 | 	SHOULD_NOT_THROW_ON_DELETE_RECORD_NOT_FOUND:
  45 | 		"shouldn't throw on delete record not found",
  46 | 	SHOULD_NOT_THROW_ON_RECORD_NOT_FOUND: "shouldn't throw on record not found",
  47 | 	SHOULD_FIND_MANY_WITH_CONTAINS_OPERATOR:
  48 | 		"should find many with contains operator",
  49 | 	SHOULD_SEARCH_USERS_WITH_STARTS_WITH: "should search users with startsWith",
  50 | 	SHOULD_SEARCH_USERS_WITH_ENDS_WITH: "should search users with endsWith",
  51 | 	SHOULD_PREFER_GENERATE_ID_IF_PROVIDED: "should prefer generateId if provided",
  52 | 	SHOULD_ROLLBACK_FAILING_TRANSACTION: "should rollback failing transaction",
  53 | 	SHOULD_RETURN_TRANSACTION_RESULT: "should return transaction result",
  54 | 	SHOULD_FIND_MANY_WITH_CONNECTORS: "should find many with connectors",
  55 | } as const;
  56 | 
  57 | const { ...numberIdAdapterTestsCopy } = adapterTests;
  58 | 
  59 | const numberIdAdapterTests = {
  60 | 	...numberIdAdapterTestsCopy,
  61 | 	SHOULD_RETURN_A_NUMBER_ID_AS_A_RESULT:
  62 | 		"Should return a number id as a result",
  63 | 	SHOULD_INCREMENT_THE_ID_BY_1: "Should increment the id by 1",
  64 | };
  65 | 
  66 | // @ts-expect-error
  67 | // biome-ignore lint/performance/noDelete: testing propose
  68 | delete numberIdAdapterTests.SHOULD_NOT_THROW_ON_DELETE_RECORD_NOT_FOUND;
  69 | 
  70 | /**
  71 |  * @deprecated Use `testAdapter` instead.
  72 |  */
  73 | function adapterTest(
  74 | 	{ getAdapter, disableTests: disabledTests, testPrefix }: AdapterTestOptions,
  75 | 	internalOptions?: {
  76 | 		predefinedOptions: Omit<BetterAuthOptions, "database">;
  77 | 	},
  78 | ) {
  79 | 	console.warn(
  80 | 		"This test function is deprecated and will be removed in the future. Use `testAdapter` instead.",
  81 | 	);
  82 | 	const adapter = async () =>
  83 | 		await getAdapter(internalOptions?.predefinedOptions);
  84 | 
  85 | 	async function resetDebugLogs() {
  86 | 		//@ts-expect-error
  87 | 		(await adapter())?.adapterTestDebugLogs?.resetDebugLogs();
  88 | 	}
  89 | 
  90 | 	async function printDebugLogs() {
  91 | 		//@ts-expect-error
  92 | 		(await adapter())?.adapterTestDebugLogs?.printDebugLogs();
  93 | 	}
  94 | 
  95 | 	// Generate unique test identifier for this test run to avoid conflicts
  96 | 	const testRunId =
  97 | 		Date.now().toString(36) + Math.random().toString(36).substring(2, 5);
  98 | 	const getUniqueEmail = (base: string) => `${testRunId}_${base}`;
  99 | 
 100 | 	//@ts-expect-error - intentionally omitting id
 101 | 	let user: {
 102 | 		name: string;
 103 | 		email: string;
 104 | 		emailVerified: boolean;
 105 | 		createdAt: Date;
 106 | 		updatedAt: Date;
 107 | 		id: string;
 108 | 	} = {
 109 | 		name: "user",
 110 | 		email: getUniqueEmail("[email protected]"),
 111 | 		emailVerified: true,
 112 | 		createdAt: new Date(),
 113 | 		updatedAt: new Date(),
 114 | 	};
 115 | 
 116 | 	test.skipIf(disabledTests?.CREATE_MODEL)(
 117 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.CREATE_MODEL}`,
 118 | 		async ({ onTestFailed }) => {
 119 | 			await resetDebugLogs();
 120 | 			onTestFailed(async () => {
 121 | 				await printDebugLogs();
 122 | 			});
 123 | 			const res = await (await adapter()).create({
 124 | 				model: "user",
 125 | 				data: user,
 126 | 			});
 127 | 			user.id = res.id;
 128 | 			expect({
 129 | 				name: res.name,
 130 | 				email: res.email,
 131 | 			}).toEqual({
 132 | 				name: user.name,
 133 | 				email: user.email,
 134 | 			});
 135 | 		},
 136 | 	);
 137 | 
 138 | 	test.skipIf(disabledTests?.CREATE_MODEL_SHOULD_ALWAYS_RETURN_AN_ID)(
 139 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 140 | 			adapterTests.CREATE_MODEL_SHOULD_ALWAYS_RETURN_AN_ID
 141 | 		}`,
 142 | 		async ({ onTestFailed }) => {
 143 | 			await resetDebugLogs();
 144 | 			onTestFailed(async () => {
 145 | 				await printDebugLogs();
 146 | 			});
 147 | 			const res = await (await adapter()).create({
 148 | 				model: "user",
 149 | 				data: {
 150 | 					name: "test-name-without-id",
 151 | 					email: getUniqueEmail("[email protected]"),
 152 | 				},
 153 | 			});
 154 | 			expect(res).toHaveProperty("id");
 155 | 			expect(typeof res?.id).toEqual("string");
 156 | 		},
 157 | 	);
 158 | 
 159 | 	test.skipIf(disabledTests?.FIND_MODEL)(
 160 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.FIND_MODEL}`,
 161 | 		async ({ onTestFailed }) => {
 162 | 			await resetDebugLogs();
 163 | 			onTestFailed(async () => {
 164 | 				await printDebugLogs();
 165 | 			});
 166 | 			const res = await (await adapter()).findOne<User>({
 167 | 				model: "user",
 168 | 				where: [
 169 | 					{
 170 | 						field: "id",
 171 | 						value: user.id,
 172 | 					},
 173 | 				],
 174 | 			});
 175 | 			expect({
 176 | 				name: res?.name,
 177 | 				email: res?.email,
 178 | 			}).toEqual({
 179 | 				name: user.name,
 180 | 				email: user.email,
 181 | 			});
 182 | 		},
 183 | 	);
 184 | 
 185 | 	test.skipIf(disabledTests?.FIND_MODEL_WITHOUT_ID)(
 186 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 187 | 			adapterTests.FIND_MODEL_WITHOUT_ID
 188 | 		}`,
 189 | 		async ({ onTestFailed }) => {
 190 | 			await resetDebugLogs();
 191 | 			onTestFailed(async () => {
 192 | 				await printDebugLogs();
 193 | 			});
 194 | 			const res = await (await adapter()).findOne<User>({
 195 | 				model: "user",
 196 | 				where: [
 197 | 					{
 198 | 						field: "email",
 199 | 						value: user.email,
 200 | 					},
 201 | 				],
 202 | 			});
 203 | 			expect({
 204 | 				name: res?.name,
 205 | 				email: res?.email,
 206 | 			}).toEqual({
 207 | 				name: user.name,
 208 | 				email: user.email,
 209 | 			});
 210 | 		},
 211 | 	);
 212 | 
 213 | 	test.skipIf(disabledTests?.FIND_MODEL_WITH_MODIFIED_FIELD_NAME)(
 214 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 215 | 			adapterTests.FIND_MODEL_WITH_MODIFIED_FIELD_NAME
 216 | 		}`,
 217 | 		async ({ onTestFailed }) => {
 218 | 			await resetDebugLogs();
 219 | 			onTestFailed(async () => {
 220 | 				await printDebugLogs();
 221 | 			});
 222 | 			const email = getUniqueEmail("[email protected]");
 223 | 			const adapter = await getAdapter(
 224 | 				Object.assign(
 225 | 					{
 226 | 						user: {
 227 | 							fields: {
 228 | 								email: "email_address",
 229 | 							},
 230 | 						},
 231 | 					},
 232 | 					internalOptions?.predefinedOptions,
 233 | 				),
 234 | 			);
 235 | 			const user = await adapter.create({
 236 | 				model: "user",
 237 | 				data: {
 238 | 					email,
 239 | 					name: "test-name-with-modified-field",
 240 | 					emailVerified: true,
 241 | 					createdAt: new Date(),
 242 | 					updatedAt: new Date(),
 243 | 				},
 244 | 			});
 245 | 			expect(user.email).toEqual(email);
 246 | 			const res = await adapter.findOne<User>({
 247 | 				model: "user",
 248 | 				where: [
 249 | 					{
 250 | 						field: "email",
 251 | 						value: email,
 252 | 					},
 253 | 				],
 254 | 			});
 255 | 			expect(res).not.toBeNull();
 256 | 			expect(res?.email).toEqual(email);
 257 | 		},
 258 | 	);
 259 | 
 260 | 	test.skipIf(disabledTests?.FIND_MODEL_WITH_SELECT)(
 261 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 262 | 			adapterTests.FIND_MODEL_WITH_SELECT
 263 | 		}`,
 264 | 		async ({ onTestFailed }) => {
 265 | 			await resetDebugLogs();
 266 | 			onTestFailed(async () => {
 267 | 				await printDebugLogs();
 268 | 			});
 269 | 			const res = await (await adapter()).findOne({
 270 | 				model: "user",
 271 | 				where: [
 272 | 					{
 273 | 						field: "id",
 274 | 						value: user.id,
 275 | 					},
 276 | 				],
 277 | 				select: ["email"],
 278 | 			});
 279 | 			expect(res).toEqual({ email: user.email });
 280 | 		},
 281 | 	);
 282 | 
 283 | 	test.skipIf(disabledTests?.UPDATE_MODEL)(
 284 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.UPDATE_MODEL}`,
 285 | 		async ({ onTestFailed }) => {
 286 | 			await resetDebugLogs();
 287 | 			onTestFailed(async () => {
 288 | 				await printDebugLogs();
 289 | 			});
 290 | 			const newEmail = getUniqueEmail("[email protected]");
 291 | 
 292 | 			const res = await (await adapter()).update<User>({
 293 | 				model: "user",
 294 | 				where: [
 295 | 					{
 296 | 						field: "id",
 297 | 						value: user.id,
 298 | 					},
 299 | 				],
 300 | 				update: {
 301 | 					email: newEmail,
 302 | 				},
 303 | 			});
 304 | 			expect(res).toMatchObject({
 305 | 				email: newEmail,
 306 | 				name: user.name,
 307 | 			});
 308 | 		},
 309 | 	);
 310 | 
 311 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY)(
 312 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.SHOULD_FIND_MANY}`,
 313 | 		async ({ onTestFailed }) => {
 314 | 			await resetDebugLogs();
 315 | 			onTestFailed(async () => {
 316 | 				await printDebugLogs();
 317 | 			});
 318 | 			const res = await (await adapter()).findMany({
 319 | 				model: "user",
 320 | 			});
 321 | 			expect(res.length).toBe(3);
 322 | 		},
 323 | 	);
 324 | 
 325 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_WHERE)(
 326 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 327 | 			adapterTests.SHOULD_FIND_MANY_WITH_WHERE
 328 | 		}`,
 329 | 		async ({ onTestFailed }) => {
 330 | 			await resetDebugLogs();
 331 | 			onTestFailed(async () => {
 332 | 				await printDebugLogs();
 333 | 			});
 334 | 			const user = await (await adapter()).create<User>({
 335 | 				model: "user",
 336 | 				data: {
 337 | 					name: "user2",
 338 | 					email: getUniqueEmail("[email protected]"),
 339 | 					emailVerified: true,
 340 | 					createdAt: new Date(),
 341 | 					updatedAt: new Date(),
 342 | 				},
 343 | 			});
 344 | 			const res = await (await adapter()).findMany({
 345 | 				model: "user",
 346 | 				where: [
 347 | 					{
 348 | 						field: "id",
 349 | 						value: user.id,
 350 | 					},
 351 | 				],
 352 | 			});
 353 | 			expect(res.length).toBe(1);
 354 | 		},
 355 | 	);
 356 | 
 357 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_OPERATORS)(
 358 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 359 | 			adapterTests.SHOULD_FIND_MANY_WITH_OPERATORS
 360 | 		}`,
 361 | 		async ({ onTestFailed }) => {
 362 | 			await resetDebugLogs();
 363 | 			onTestFailed(async () => {
 364 | 				await printDebugLogs();
 365 | 			});
 366 | 			const newUser = await (await adapter()).create<User>({
 367 | 				model: "user",
 368 | 				data: {
 369 | 					name: "user",
 370 | 					email: getUniqueEmail("[email protected]"),
 371 | 					emailVerified: true,
 372 | 					createdAt: new Date(),
 373 | 					updatedAt: new Date(),
 374 | 				},
 375 | 			});
 376 | 			const res = await (await adapter()).findMany({
 377 | 				model: "user",
 378 | 				where: [
 379 | 					{
 380 | 						field: "id",
 381 | 						operator: "in",
 382 | 						value: [user.id, newUser.id],
 383 | 					},
 384 | 				],
 385 | 			});
 386 | 			expect(res.length).toBe(2);
 387 | 		},
 388 | 	);
 389 | 
 390 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_NOT_IN_OPERATOR)(
 391 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 392 | 			adapterTests.SHOULD_FIND_MANY_WITH_NOT_IN_OPERATOR
 393 | 		}`,
 394 | 		async ({ onTestFailed }) => {
 395 | 			await resetDebugLogs();
 396 | 			onTestFailed(async () => {
 397 | 				await printDebugLogs();
 398 | 			});
 399 | 
 400 | 			const newUser3 = await (await adapter()).create<User>({
 401 | 				model: "user",
 402 | 				data: {
 403 | 					name: "user",
 404 | 					email: getUniqueEmail("[email protected]"),
 405 | 					emailVerified: true,
 406 | 					createdAt: new Date(),
 407 | 					updatedAt: new Date(),
 408 | 				},
 409 | 			});
 410 | 			const allUsers = await (await adapter()).findMany<User>({
 411 | 				model: "user",
 412 | 			});
 413 | 			expect(allUsers.length).toBe(6);
 414 | 			const usersWithoutNotIn = await (await adapter()).findMany<User>({
 415 | 				model: "user",
 416 | 				where: [
 417 | 					{
 418 | 						field: "id",
 419 | 						operator: "not_in",
 420 | 						value: [user.id, newUser3.id],
 421 | 					},
 422 | 				],
 423 | 			});
 424 | 			expect(usersWithoutNotIn.length).toBe(4);
 425 | 			//cleanup
 426 | 			await (await adapter()).delete({
 427 | 				model: "user",
 428 | 				where: [
 429 | 					{
 430 | 						field: "id",
 431 | 						value: newUser3.id,
 432 | 					},
 433 | 				],
 434 | 			});
 435 | 		},
 436 | 	);
 437 | 
 438 | 	test.skipIf(disabledTests?.SHOULD_WORK_WITH_REFERENCE_FIELDS)(
 439 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 440 | 			adapterTests.SHOULD_WORK_WITH_REFERENCE_FIELDS
 441 | 		}`,
 442 | 		async ({ onTestFailed }) => {
 443 | 			await resetDebugLogs();
 444 | 			onTestFailed(async () => {
 445 | 				await printDebugLogs();
 446 | 			});
 447 | 			let token = null;
 448 | 			const user = await (await adapter()).create<Record<string, any>>({
 449 | 				model: "user",
 450 | 				data: {
 451 | 					name: "user",
 452 | 					email: getUniqueEmail("[email protected]"),
 453 | 					emailVerified: true,
 454 | 					createdAt: new Date(),
 455 | 					updatedAt: new Date(),
 456 | 				},
 457 | 			});
 458 | 			const session = await (await adapter()).create({
 459 | 				model: "session",
 460 | 				data: {
 461 | 					token: generateId(),
 462 | 					createdAt: new Date(),
 463 | 					updatedAt: new Date(),
 464 | 					userId: user.id,
 465 | 					expiresAt: new Date(),
 466 | 				},
 467 | 			});
 468 | 			token = session.token;
 469 | 			const res = await (await adapter()).findOne({
 470 | 				model: "session",
 471 | 				where: [
 472 | 					{
 473 | 						field: "userId",
 474 | 						value: user.id,
 475 | 					},
 476 | 				],
 477 | 			});
 478 | 			const resToken = await (await adapter()).findOne({
 479 | 				model: "session",
 480 | 				where: [
 481 | 					{
 482 | 						field: "token",
 483 | 						value: token,
 484 | 					},
 485 | 				],
 486 | 			});
 487 | 			expect(res).toMatchObject({
 488 | 				userId: user.id,
 489 | 			});
 490 | 			expect(resToken).toMatchObject({
 491 | 				userId: user.id,
 492 | 			});
 493 | 		},
 494 | 	);
 495 | 
 496 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_SORT_BY)(
 497 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 498 | 			adapterTests.SHOULD_FIND_MANY_WITH_SORT_BY
 499 | 		}`,
 500 | 		async ({ onTestFailed }) => {
 501 | 			await resetDebugLogs();
 502 | 			onTestFailed(async () => {
 503 | 				await printDebugLogs();
 504 | 			});
 505 | 			await (await adapter()).create({
 506 | 				model: "user",
 507 | 				data: {
 508 | 					name: "a",
 509 | 					email: getUniqueEmail("[email protected]"),
 510 | 					emailVerified: true,
 511 | 					createdAt: new Date(),
 512 | 					updatedAt: new Date(),
 513 | 				},
 514 | 			});
 515 | 			const res = await (await adapter()).findMany<User>({
 516 | 				model: "user",
 517 | 				sortBy: {
 518 | 					field: "name",
 519 | 					direction: "asc",
 520 | 				},
 521 | 			});
 522 | 			expect(res[0]!.name).toBe("a");
 523 | 
 524 | 			const res2 = await (await adapter()).findMany<User>({
 525 | 				model: "user",
 526 | 				sortBy: {
 527 | 					field: "name",
 528 | 					direction: "desc",
 529 | 				},
 530 | 			});
 531 | 
 532 | 			expect(res2[res2.length - 1]!.name).toBe("a");
 533 | 		},
 534 | 	);
 535 | 
 536 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_LIMIT)(
 537 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 538 | 			adapterTests.SHOULD_FIND_MANY_WITH_LIMIT
 539 | 		}`,
 540 | 		async ({ onTestFailed }) => {
 541 | 			await resetDebugLogs();
 542 | 			onTestFailed(async () => {
 543 | 				await printDebugLogs();
 544 | 			});
 545 | 			const res = await (await adapter()).findMany({
 546 | 				model: "user",
 547 | 				limit: 1,
 548 | 			});
 549 | 			expect(res.length).toBe(1);
 550 | 		},
 551 | 	);
 552 | 
 553 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_OFFSET)(
 554 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 555 | 			adapterTests.SHOULD_FIND_MANY_WITH_OFFSET
 556 | 		}`,
 557 | 		async ({ onTestFailed }) => {
 558 | 			await resetDebugLogs();
 559 | 			onTestFailed(async () => {
 560 | 				await printDebugLogs();
 561 | 			});
 562 | 			const res = await (await adapter()).findMany({
 563 | 				model: "user",
 564 | 				offset: 2,
 565 | 			});
 566 | 			expect(res.length).toBe(5);
 567 | 		},
 568 | 	);
 569 | 
 570 | 	test.skipIf(disabledTests?.SHOULD_UPDATE_WITH_MULTIPLE_WHERE)(
 571 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 572 | 			adapterTests.SHOULD_UPDATE_WITH_MULTIPLE_WHERE
 573 | 		}`,
 574 | 		async ({ onTestFailed }) => {
 575 | 			await resetDebugLogs();
 576 | 			onTestFailed(async () => {
 577 | 				await printDebugLogs();
 578 | 			});
 579 | 			// Note: user's email was already updated in the previous test
 580 | 			const currentEmail = getUniqueEmail("[email protected]");
 581 | 			await (await adapter()).updateMany({
 582 | 				model: "user",
 583 | 				where: [
 584 | 					{
 585 | 						field: "name",
 586 | 						value: user.name,
 587 | 					},
 588 | 					{
 589 | 						field: "email",
 590 | 						value: currentEmail,
 591 | 					},
 592 | 				],
 593 | 				update: {
 594 | 					email: getUniqueEmail("[email protected]"),
 595 | 				},
 596 | 			});
 597 | 			const updatedUser = await (await adapter()).findOne<User>({
 598 | 				model: "user",
 599 | 				where: [
 600 | 					{
 601 | 						field: "email",
 602 | 						value: getUniqueEmail("[email protected]"),
 603 | 					},
 604 | 				],
 605 | 			});
 606 | 			expect(updatedUser).toMatchObject({
 607 | 				name: user.name,
 608 | 				email: getUniqueEmail("[email protected]"),
 609 | 			});
 610 | 		},
 611 | 	);
 612 | 
 613 | 	test.skipIf(disabledTests?.DELETE_MODEL)(
 614 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.DELETE_MODEL}`,
 615 | 		async ({ onTestFailed }) => {
 616 | 			await resetDebugLogs();
 617 | 			onTestFailed(async () => {
 618 | 				await printDebugLogs();
 619 | 			});
 620 | 			await (await adapter()).delete({
 621 | 				model: "user",
 622 | 				where: [
 623 | 					{
 624 | 						field: "id",
 625 | 						value: user.id,
 626 | 					},
 627 | 				],
 628 | 			});
 629 | 			const findRes = await (await adapter()).findOne({
 630 | 				model: "user",
 631 | 				where: [
 632 | 					{
 633 | 						field: "id",
 634 | 						value: user.id,
 635 | 					},
 636 | 				],
 637 | 			});
 638 | 			expect(findRes).toBeNull();
 639 | 		},
 640 | 	);
 641 | 
 642 | 	test.skipIf(disabledTests?.SHOULD_DELETE_MANY)(
 643 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.SHOULD_DELETE_MANY}`,
 644 | 		async ({ onTestFailed }) => {
 645 | 			await resetDebugLogs();
 646 | 			onTestFailed(async () => {
 647 | 				await printDebugLogs();
 648 | 			});
 649 | 			for (const i of ["to-be-delete-1", "to-be-delete-2", "to-be-delete-3"]) {
 650 | 				await (await adapter()).create({
 651 | 					model: "user",
 652 | 					data: {
 653 | 						name: "to-be-deleted",
 654 | 						email: getUniqueEmail(`email@test-${i}.com`),
 655 | 						emailVerified: true,
 656 | 						createdAt: new Date(),
 657 | 						updatedAt: new Date(),
 658 | 					},
 659 | 				});
 660 | 			}
 661 | 			const findResFirst = await (await adapter()).findMany({
 662 | 				model: "user",
 663 | 				where: [
 664 | 					{
 665 | 						field: "name",
 666 | 						value: "to-be-deleted",
 667 | 					},
 668 | 				],
 669 | 			});
 670 | 			expect(findResFirst.length).toBe(3);
 671 | 			await (await adapter()).deleteMany({
 672 | 				model: "user",
 673 | 				where: [
 674 | 					{
 675 | 						field: "name",
 676 | 						value: "to-be-deleted",
 677 | 					},
 678 | 				],
 679 | 			});
 680 | 			const findRes = await (await adapter()).findMany({
 681 | 				model: "user",
 682 | 				where: [
 683 | 					{
 684 | 						field: "name",
 685 | 						value: "to-be-deleted",
 686 | 					},
 687 | 				],
 688 | 			});
 689 | 			expect(findRes.length).toBe(0);
 690 | 		},
 691 | 	);
 692 | 
 693 | 	test.skipIf(disabledTests?.SHOULD_NOT_THROW_ON_DELETE_RECORD_NOT_FOUND)(
 694 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 695 | 			adapterTests.SHOULD_NOT_THROW_ON_DELETE_RECORD_NOT_FOUND
 696 | 		}`,
 697 | 		async ({ onTestFailed }) => {
 698 | 			await resetDebugLogs();
 699 | 			onTestFailed(async () => {
 700 | 				await printDebugLogs();
 701 | 			});
 702 | 			await (await adapter()).delete({
 703 | 				model: "user",
 704 | 				where: [
 705 | 					{
 706 | 						field: "id",
 707 | 						value: "100000",
 708 | 					},
 709 | 				],
 710 | 			});
 711 | 		},
 712 | 	);
 713 | 
 714 | 	test.skipIf(disabledTests?.SHOULD_NOT_THROW_ON_RECORD_NOT_FOUND)(
 715 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 716 | 			adapterTests.SHOULD_NOT_THROW_ON_RECORD_NOT_FOUND
 717 | 		}`,
 718 | 		async ({ onTestFailed }) => {
 719 | 			await resetDebugLogs();
 720 | 			onTestFailed(async () => {
 721 | 				await printDebugLogs();
 722 | 			});
 723 | 			const res = await (await adapter()).findOne({
 724 | 				model: "user",
 725 | 				where: [
 726 | 					{
 727 | 						field: "id",
 728 | 						value: "100000",
 729 | 					},
 730 | 				],
 731 | 			});
 732 | 			expect(res).toBeNull();
 733 | 		},
 734 | 	);
 735 | 
 736 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_CONTAINS_OPERATOR)(
 737 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 738 | 			adapterTests.SHOULD_FIND_MANY_WITH_CONTAINS_OPERATOR
 739 | 		}`,
 740 | 		async ({ onTestFailed }) => {
 741 | 			await resetDebugLogs();
 742 | 			onTestFailed(async () => {
 743 | 				await printDebugLogs();
 744 | 			});
 745 | 			const res = await (await adapter()).findMany({
 746 | 				model: "user",
 747 | 				where: [
 748 | 					{
 749 | 						field: "name",
 750 | 						operator: "contains",
 751 | 						value: "user2",
 752 | 					},
 753 | 				],
 754 | 			});
 755 | 			expect(res.length).toBe(1);
 756 | 		},
 757 | 	);
 758 | 
 759 | 	test.skipIf(disabledTests?.SHOULD_SEARCH_USERS_WITH_STARTS_WITH)(
 760 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 761 | 			adapterTests.SHOULD_SEARCH_USERS_WITH_STARTS_WITH
 762 | 		}`,
 763 | 		async ({ onTestFailed }) => {
 764 | 			await resetDebugLogs();
 765 | 			onTestFailed(async () => {
 766 | 				await printDebugLogs();
 767 | 			});
 768 | 			await (await adapter()).create({
 769 | 				model: "user",
 770 | 				data: {
 771 | 					name: "user_starts",
 772 | 					email: getUniqueEmail("[email protected]"),
 773 | 					emailVerified: true,
 774 | 					createdAt: new Date(),
 775 | 					updatedAt: new Date(),
 776 | 				},
 777 | 			});
 778 | 			await (await adapter()).create({
 779 | 				model: "user",
 780 | 				data: {
 781 | 					name: "user2_starts",
 782 | 					email: getUniqueEmail("[email protected]"),
 783 | 					emailVerified: true,
 784 | 					createdAt: new Date(),
 785 | 					updatedAt: new Date(),
 786 | 				},
 787 | 			});
 788 | 			await (await adapter()).create({
 789 | 				model: "user",
 790 | 				data: {
 791 | 					name: "user3_starts",
 792 | 					email: getUniqueEmail("[email protected]"),
 793 | 					emailVerified: true,
 794 | 					createdAt: new Date(),
 795 | 					updatedAt: new Date(),
 796 | 				},
 797 | 			});
 798 | 			const res = await (await adapter()).findMany({
 799 | 				model: "user",
 800 | 				where: [
 801 | 					{
 802 | 						field: "name",
 803 | 						operator: "starts_with",
 804 | 						value: "user",
 805 | 					},
 806 | 				],
 807 | 			});
 808 | 			expect(res.length).toBeGreaterThanOrEqual(3);
 809 | 		},
 810 | 	);
 811 | 
 812 | 	test.skipIf(disabledTests?.SHOULD_SEARCH_USERS_WITH_ENDS_WITH)(
 813 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 814 | 			adapterTests.SHOULD_SEARCH_USERS_WITH_ENDS_WITH
 815 | 		}`,
 816 | 		async ({ onTestFailed }) => {
 817 | 			await resetDebugLogs();
 818 | 			onTestFailed(async () => {
 819 | 				await printDebugLogs();
 820 | 			});
 821 | 			// Create test user for this test with unique suffix
 822 | 			await (await adapter()).create({
 823 | 				model: "user",
 824 | 				data: {
 825 | 					name: "tester2",
 826 | 					email: getUniqueEmail("[email protected]"),
 827 | 					emailVerified: true,
 828 | 					createdAt: new Date(),
 829 | 					updatedAt: new Date(),
 830 | 				},
 831 | 			});
 832 | 			const res = await (await adapter()).findMany({
 833 | 				model: "user",
 834 | 				where: [
 835 | 					{
 836 | 						field: "name",
 837 | 						operator: "ends_with",
 838 | 						value: "ter2",
 839 | 					},
 840 | 				],
 841 | 			});
 842 | 			expect(res.length).toBe(1);
 843 | 		},
 844 | 	);
 845 | 
 846 | 	test.skipIf(disabledTests?.SHOULD_PREFER_GENERATE_ID_IF_PROVIDED)(
 847 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 848 | 			adapterTests.SHOULD_PREFER_GENERATE_ID_IF_PROVIDED
 849 | 		}`,
 850 | 		async ({ onTestFailed }) => {
 851 | 			await resetDebugLogs();
 852 | 			onTestFailed(async () => {
 853 | 				await printDebugLogs();
 854 | 			});
 855 | 			const customAdapter = await getAdapter(
 856 | 				Object.assign(
 857 | 					{
 858 | 						advanced: {
 859 | 							database: {
 860 | 								generateId: () => "mocked-id",
 861 | 							},
 862 | 						},
 863 | 					} satisfies BetterAuthOptions,
 864 | 					internalOptions?.predefinedOptions,
 865 | 				),
 866 | 			);
 867 | 
 868 | 			const res = await customAdapter.create({
 869 | 				model: "user",
 870 | 				data: {
 871 | 					name: "user4",
 872 | 					email: getUniqueEmail("[email protected]"),
 873 | 					emailVerified: true,
 874 | 					createdAt: new Date(),
 875 | 					updatedAt: new Date(),
 876 | 				},
 877 | 			});
 878 | 
 879 | 			expect(res.id).toBe("mocked-id");
 880 | 		},
 881 | 	);
 882 | 
 883 | 	test.skipIf(disabledTests?.SHOULD_ROLLBACK_FAILING_TRANSACTION)(
 884 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.SHOULD_ROLLBACK_FAILING_TRANSACTION}`,
 885 | 		async ({ onTestFailed, skip }) => {
 886 | 			await resetDebugLogs();
 887 | 			onTestFailed(async () => {
 888 | 				await printDebugLogs();
 889 | 			});
 890 | 			const customAdapter = await adapter();
 891 | 
 892 | 			// Check if adapter actually supports transactions
 893 | 			const enableTransaction =
 894 | 				customAdapter?.options?.adapterConfig.transaction;
 895 | 			if (!enableTransaction) {
 896 | 				skip(
 897 | 					`Skipping test: ${
 898 | 						customAdapter?.options?.adapterConfig.adapterName || "Adapter"
 899 | 					}
 900 | 					 does not support transactions`,
 901 | 				);
 902 | 				return;
 903 | 			}
 904 | 
 905 | 			const user5 = {
 906 | 				name: "user5",
 907 | 				email: getUniqueEmail("[email protected]"),
 908 | 				emailVerified: true,
 909 | 				createdAt: new Date(),
 910 | 				updatedAt: new Date(),
 911 | 			};
 912 | 			const user6 = {
 913 | 				name: "user6",
 914 | 				email: getUniqueEmail("[email protected]"),
 915 | 				emailVerified: true,
 916 | 				createdAt: new Date(),
 917 | 				updatedAt: new Date(),
 918 | 			};
 919 | 			await expect(
 920 | 				customAdapter.transaction(async (tx) => {
 921 | 					await tx.create({ model: "user", data: user5 });
 922 | 					throw new Error("Simulated failure");
 923 | 					await tx.create({ model: "user", data: user6 });
 924 | 				}),
 925 | 			).rejects.toThrow("Simulated failure");
 926 | 
 927 | 			await expect(
 928 | 				customAdapter.findMany({
 929 | 					model: "user",
 930 | 					where: [
 931 | 						{
 932 | 							field: "email",
 933 | 							value: user5.email,
 934 | 							connector: "OR",
 935 | 						},
 936 | 						{
 937 | 							field: "email",
 938 | 							value: user6.email,
 939 | 							connector: "OR",
 940 | 						},
 941 | 					],
 942 | 				}),
 943 | 			).resolves.toEqual([]);
 944 | 		},
 945 | 	);
 946 | 
 947 | 	test.skipIf(disabledTests?.SHOULD_RETURN_TRANSACTION_RESULT)(
 948 | 		`${testPrefix ? `${testPrefix} - ` : ""}${adapterTests.SHOULD_RETURN_TRANSACTION_RESULT}`,
 949 | 		async ({ onTestFailed, skip }) => {
 950 | 			await resetDebugLogs();
 951 | 			onTestFailed(async () => {
 952 | 				await printDebugLogs();
 953 | 			});
 954 | 			const customAdapter = await adapter();
 955 | 
 956 | 			const enableTransaction =
 957 | 				customAdapter?.options?.adapterConfig.transaction;
 958 | 			if (!enableTransaction) {
 959 | 				skip(
 960 | 					`Skipping test: ${
 961 | 						customAdapter?.options?.adapterConfig.adapterName || "Adapter"
 962 | 					}
 963 | 					 does not support transactions`,
 964 | 				);
 965 | 				return;
 966 | 			}
 967 | 
 968 | 			const result = await customAdapter.transaction(async (tx) => {
 969 | 				const createdUser = await tx.create<User>({
 970 | 					model: "user",
 971 | 					data: {
 972 | 						name: "user6",
 973 | 						email: getUniqueEmail("[email protected]"),
 974 | 						emailVerified: true,
 975 | 						createdAt: new Date(),
 976 | 						updatedAt: new Date(),
 977 | 					},
 978 | 				});
 979 | 
 980 | 				return createdUser.email;
 981 | 			});
 982 | 
 983 | 			expect(result).toEqual(getUniqueEmail("[email protected]"));
 984 | 		},
 985 | 	);
 986 | 
 987 | 	test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_CONNECTORS)(
 988 | 		`${testPrefix ? `${testPrefix} - ` : ""}${
 989 | 			adapterTests.SHOULD_FIND_MANY_WITH_CONNECTORS
 990 | 		}`,
 991 | 		async ({ onTestFailed }) => {
 992 | 			await resetDebugLogs();
 993 | 			onTestFailed(async () => {
 994 | 				await printDebugLogs();
 995 | 			});
 996 | 
 997 | 			await (await adapter()).create({
 998 | 				model: "user",
 999 | 				data: {
1000 | 					name: "connector-user1",
1001 | 					email: getUniqueEmail("[email protected]"),
1002 | 					emailVerified: true,
1003 | 					createdAt: new Date(),
1004 | 					updatedAt: new Date(),
1005 | 				},
1006 | 			});
1007 | 			await (await adapter()).create({
1008 | 				model: "user",
1009 | 				data: {
1010 | 					name: "con-user2",
1011 | 					email: getUniqueEmail("[email protected]"),
1012 | 					emailVerified: true,
1013 | 					createdAt: new Date(),
1014 | 					updatedAt: new Date(),
1015 | 				},
1016 | 			});
1017 | 
1018 | 			const andRes = await (await adapter()).findMany({
1019 | 				model: "user",
1020 | 				where: [
1021 | 					{
1022 | 						field: "name",
1023 | 						value: "con-user2",
1024 | 						connector: "AND",
1025 | 					},
1026 | 					{
1027 | 						field: "email",
1028 | 						value: getUniqueEmail("[email protected]"),
1029 | 						connector: "AND",
1030 | 					},
1031 | 				],
1032 | 			});
1033 | 
1034 | 			expect(andRes.length).toBe(1);
1035 | 
1036 | 			const orRes = await (await adapter()).findMany({
1037 | 				model: "user",
1038 | 				where: [
1039 | 					{
1040 | 						field: "name",
1041 | 						value: "connector-user1",
1042 | 						connector: "OR",
1043 | 					},
1044 | 					{
1045 | 						field: "name",
1046 | 						value: "con-user2",
1047 | 						connector: "OR",
1048 | 					},
1049 | 				],
1050 | 			});
1051 | 			expect(orRes.length).toBe(2);
1052 | 		},
1053 | 	);
1054 | }
1055 | 
1056 | export function runAdapterTest(opts: AdapterTestOptions) {
1057 | 	return adapterTest(opts);
1058 | }
1059 | 
1060 | export function runNumberIdAdapterTest(opts: NumberIdAdapterTestOptions) {
1061 | 	const cleanup: { modelName: string; id: string }[] = [];
1062 | 
1063 | 	// Generate unique test identifier for this test run to avoid conflicts
1064 | 	const testRunId =
1065 | 		Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
1066 | 	const getUniqueEmail = (base: string) => `${testRunId}_${base}`;
1067 | 
1068 | 	const adapter = async () =>
1069 | 		await opts.getAdapter({
1070 | 			advanced: {
1071 | 				database: {
1072 | 					useNumberId: true,
1073 | 				},
1074 | 			},
1075 | 		});
1076 | 	describe("Should run number id specific tests", async () => {
1077 | 		let idNumber = -1;
1078 | 
1079 | 		async function resetDebugLogs() {
1080 | 			//@ts-expect-error
1081 | 			(await adapter())?.adapterTestDebugLogs?.resetDebugLogs();
1082 | 		}
1083 | 
1084 | 		async function printDebugLogs() {
1085 | 			//@ts-expect-error
1086 | 			(await adapter())?.adapterTestDebugLogs?.printDebugLogs();
1087 | 		}
1088 | 		test.skipIf(opts.disableTests?.SHOULD_RETURN_A_NUMBER_ID_AS_A_RESULT)(
1089 | 			`${opts.testPrefix ? `${opts.testPrefix} - ` : ""}${
1090 | 				numberIdAdapterTests.SHOULD_RETURN_A_NUMBER_ID_AS_A_RESULT
1091 | 			}`,
1092 | 			async ({ onTestFailed }) => {
1093 | 				await resetDebugLogs();
1094 | 				onTestFailed(async () => {
1095 | 					await printDebugLogs();
1096 | 				});
1097 | 				const res = await (await adapter()).create({
1098 | 					model: "user",
1099 | 					data: {
1100 | 						name: "user",
1101 | 						email: getUniqueEmail("[email protected]"),
1102 | 					},
1103 | 				});
1104 | 				cleanup.push({ modelName: "user", id: res.id });
1105 | 				expect(typeof res.id).toBe("string"); // we forcefully return all `id`s as strings. this is intentional.
1106 | 				expect(parseInt(res.id)).toBeGreaterThan(0);
1107 | 				idNumber = parseInt(res.id);
1108 | 			},
1109 | 		);
1110 | 		test.skipIf(opts.disableTests?.SHOULD_INCREMENT_THE_ID_BY_1)(
1111 | 			`${opts.testPrefix ? `${opts.testPrefix} - ` : ""}${
1112 | 				numberIdAdapterTests.SHOULD_INCREMENT_THE_ID_BY_1
1113 | 			}`,
1114 | 			async ({ onTestFailed }) => {
1115 | 				await resetDebugLogs();
1116 | 				onTestFailed(async () => {
1117 | 					console.log(`ID number from last create: ${idNumber}`);
1118 | 					await printDebugLogs();
1119 | 				});
1120 | 				const res = await (await adapter()).create({
1121 | 					model: "user",
1122 | 					data: {
1123 | 						name: "user2",
1124 | 						email: getUniqueEmail("[email protected]"),
1125 | 					},
1126 | 				});
1127 | 				cleanup.push({ modelName: "user", id: res.id });
1128 | 				expect(parseInt(res.id)).toBe(idNumber + 1);
1129 | 			},
1130 | 		);
1131 | 	});
1132 | 
1133 | 	describe("Should run normal adapter tests with number id enabled", async () => {
1134 | 		beforeAll(async () => {
1135 | 			for (const { modelName, id } of cleanup) {
1136 | 				await (await adapter()).delete({
1137 | 					model: modelName,
1138 | 					where: [{ field: "id", value: id }],
1139 | 				});
1140 | 			}
1141 | 		});
1142 | 		await adapterTest(
1143 | 			{
1144 | 				...opts,
1145 | 				disableTests: {
1146 | 					...opts.disableTests,
1147 | 					SHOULD_PREFER_GENERATE_ID_IF_PROVIDED: true,
1148 | 				},
1149 | 			},
1150 | 			{
1151 | 				predefinedOptions: {
1152 | 					advanced: {
1153 | 						database: {
1154 | 							useNumberId: true,
1155 | 						},
1156 | 					},
1157 | 				},
1158 | 			},
1159 | 		);
1160 | 	});
1161 | }
1162 | 
1163 | export function recoverProcessTZ() {
1164 | 	const originalTZ = process.env.TZ;
1165 | 	return {
1166 | 		[Symbol.dispose]: () => {
1167 | 			process.env.TZ = originalTZ;
1168 | 		},
1169 | 	};
1170 | }
1171 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/app/dashboard/user-card.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
  4 | import { Button } from "@/components/ui/button";
  5 | import {
  6 | 	Card,
  7 | 	CardContent,
  8 | 	CardFooter,
  9 | 	CardHeader,
 10 | 	CardTitle,
 11 | } from "@/components/ui/card";
 12 | import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
 13 | import { Checkbox } from "@/components/ui/checkbox";
 14 | import {
 15 | 	Dialog,
 16 | 	DialogContent,
 17 | 	DialogDescription,
 18 | 	DialogFooter,
 19 | 	DialogHeader,
 20 | 	DialogTitle,
 21 | 	DialogTrigger,
 22 | } from "@/components/ui/dialog";
 23 | import { Input } from "@/components/ui/input";
 24 | import { Label } from "@/components/ui/label";
 25 | import { PasswordInput } from "@/components/ui/password-input";
 26 | import { client, signOut, useSession } from "@/lib/auth-client";
 27 | import { Session } from "@/lib/auth-types";
 28 | import { MobileIcon } from "@radix-ui/react-icons";
 29 | import {
 30 | 	Edit,
 31 | 	Fingerprint,
 32 | 	Laptop,
 33 | 	Loader2,
 34 | 	LogOut,
 35 | 	Plus,
 36 | 	QrCode,
 37 | 	ShieldCheck,
 38 | 	ShieldOff,
 39 | 	StopCircle,
 40 | 	Trash,
 41 | 	X,
 42 | } from "lucide-react";
 43 | import Image from "next/image";
 44 | import { useRouter } from "next/navigation";
 45 | import { useState, useTransition } from "react";
 46 | import { toast } from "sonner";
 47 | import { UAParser } from "ua-parser-js";
 48 | import {
 49 | 	Table,
 50 | 	TableBody,
 51 | 	TableCell,
 52 | 	TableHead,
 53 | 	TableHeader,
 54 | 	TableRow,
 55 | } from "@/components/ui/table";
 56 | import QRCode from "react-qr-code";
 57 | import CopyButton from "@/components/ui/copy-button";
 58 | import { Badge } from "@/components/ui/badge";
 59 | import { useQuery } from "@tanstack/react-query";
 60 | import { SubscriptionTierLabel } from "@/components/tier-labels";
 61 | import { Component } from "./change-plan";
 62 | import { Subscription } from "@better-auth/stripe";
 63 | 
 64 | export default function UserCard(props: {
 65 | 	session: Session | null;
 66 | 	activeSessions: Session["session"][];
 67 | 	subscription?: Subscription;
 68 | }) {
 69 | 	const router = useRouter();
 70 | 	const { data, isPending } = useSession();
 71 | 	const session = data || props.session;
 72 | 	const [isTerminating, setIsTerminating] = useState<string>();
 73 | 	const [isPendingTwoFa, setIsPendingTwoFa] = useState<boolean>(false);
 74 | 	const [twoFaPassword, setTwoFaPassword] = useState<string>("");
 75 | 	const [twoFactorDialog, setTwoFactorDialog] = useState<boolean>(false);
 76 | 	const [twoFactorVerifyURI, setTwoFactorVerifyURI] = useState<string>("");
 77 | 	const [isSignOut, setIsSignOut] = useState<boolean>(false);
 78 | 	const [emailVerificationPending, setEmailVerificationPending] =
 79 | 		useState<boolean>(false);
 80 | 	const [activeSessions, setActiveSessions] = useState(props.activeSessions);
 81 | 	const removeActiveSession = (id: string) =>
 82 | 		setActiveSessions(activeSessions.filter((session) => session.id !== id));
 83 | 	const { data: subscription } = useQuery({
 84 | 		queryKey: ["subscriptions"],
 85 | 		initialData: props.subscription ? props.subscription : null,
 86 | 		queryFn: async () => {
 87 | 			const res = await client.subscription.list({
 88 | 				fetchOptions: {
 89 | 					throw: true,
 90 | 				},
 91 | 			});
 92 | 			return res.length ? res[0] : null;
 93 | 		},
 94 | 	});
 95 | 
 96 | 	return (
 97 | 		<Card>
 98 | 			<CardHeader>
 99 | 				<CardTitle>User</CardTitle>
100 | 			</CardHeader>
101 | 			<CardContent className="grid gap-8 grid-cols-1">
102 | 				<div className="flex flex-col gap-2">
103 | 					<div className="flex items-start justify-between">
104 | 						<div className="flex items-center gap-4">
105 | 							<Avatar className="hidden h-9 w-9 sm:flex ">
106 | 								<AvatarImage
107 | 									src={session?.user.image || undefined}
108 | 									alt="Avatar"
109 | 									className="object-cover"
110 | 								/>
111 | 								<AvatarFallback>{session?.user.name.charAt(0)}</AvatarFallback>
112 | 							</Avatar>
113 | 							<div className="grid">
114 | 								<div className="flex items-center gap-1">
115 | 									<p className="text-sm font-medium leading-none">
116 | 										{session?.user.name}
117 | 									</p>
118 | 									{!!subscription && (
119 | 										<Badge
120 | 											className="w-min p-px rounded-full"
121 | 											variant="outline"
122 | 										>
123 | 											<svg
124 | 												xmlns="http://www.w3.org/2000/svg"
125 | 												width="1.2em"
126 | 												height="1.2em"
127 | 												viewBox="0 0 24 24"
128 | 											>
129 | 												<path
130 | 													fill="currentColor"
131 | 													d="m9.023 21.23l-1.67-2.814l-3.176-.685l.312-3.277L2.346 12L4.49 9.546L4.177 6.27l3.177-.685L9.023 2.77L12 4.027l2.977-1.258l1.67 2.816l3.176.684l-.312 3.277L21.655 12l-2.142 2.454l.311 3.277l-3.177.684l-1.669 2.816L12 19.973zm1.927-6.372L15.908 9.9l-.708-.72l-4.25 4.25l-2.15-2.138l-.708.708z"
132 | 												></path>
133 | 											</svg>
134 | 										</Badge>
135 | 									)}
136 | 								</div>
137 | 								<p className="text-sm">{session?.user.email}</p>
138 | 							</div>
139 | 						</div>
140 | 						<EditUserDialog />
141 | 					</div>
142 | 					<div className="flex items-center justify-between">
143 | 						<div>
144 | 							<SubscriptionTierLabel
145 | 								tier={subscription?.plan?.toLowerCase() as "plus"}
146 | 							/>
147 | 						</div>
148 | 						<Component
149 | 							currentPlan={subscription?.plan?.toLowerCase() as "plus"}
150 | 							isTrial={subscription?.status === "trialing"}
151 | 						/>
152 | 					</div>
153 | 				</div>
154 | 
155 | 				{session?.user.emailVerified ? null : (
156 | 					<Alert>
157 | 						<AlertTitle>Verify Your Email Address</AlertTitle>
158 | 						<AlertDescription className="text-muted-foreground">
159 | 							Please verify your email address. Check your inbox for the
160 | 							verification email. If you haven't received the email, click the
161 | 							button below to resend.
162 | 							<Button
163 | 								size="sm"
164 | 								variant="secondary"
165 | 								className="mt-2"
166 | 								onClick={async () => {
167 | 									await client.sendVerificationEmail(
168 | 										{
169 | 											email: session?.user.email || "",
170 | 										},
171 | 										{
172 | 											onRequest(context) {
173 | 												setEmailVerificationPending(true);
174 | 											},
175 | 											onError(context) {
176 | 												toast.error(context.error.message);
177 | 												setEmailVerificationPending(false);
178 | 											},
179 | 											onSuccess() {
180 | 												toast.success("Verification email sent successfully");
181 | 												setEmailVerificationPending(false);
182 | 											},
183 | 										},
184 | 									);
185 | 								}}
186 | 							>
187 | 								{emailVerificationPending ? (
188 | 									<Loader2 size={15} className="animate-spin" />
189 | 								) : (
190 | 									"Resend Verification Email"
191 | 								)}
192 | 							</Button>
193 | 						</AlertDescription>
194 | 					</Alert>
195 | 				)}
196 | 
197 | 				<div className="border-l-2 px-2 w-max gap-1 flex flex-col">
198 | 					<p className="text-xs font-medium ">Active Sessions</p>
199 | 					{activeSessions
200 | 						.filter((session) => session.userAgent)
201 | 						.map((session) => {
202 | 							return (
203 | 								<div key={session.id}>
204 | 									<div className="flex items-center gap-2 text-sm  text-black font-medium dark:text-white">
205 | 										{new UAParser(session.userAgent || "").getDevice().type ===
206 | 										"mobile" ? (
207 | 											<MobileIcon />
208 | 										) : (
209 | 											<Laptop size={16} />
210 | 										)}
211 | 										{new UAParser(session.userAgent || "").getOS().name ||
212 | 											session.userAgent}
213 | 										, {new UAParser(session.userAgent || "").getBrowser().name}
214 | 										<button
215 | 											className="text-red-500 opacity-80  cursor-pointer text-xs border-muted-foreground border-red-600  underline "
216 | 											onClick={async () => {
217 | 												setIsTerminating(session.id);
218 | 												const res = await client.revokeSession({
219 | 													token: session.token,
220 | 												});
221 | 
222 | 												if (res.error) {
223 | 													toast.error(res.error.message);
224 | 												} else {
225 | 													toast.success("Session terminated successfully");
226 | 													removeActiveSession(session.id);
227 | 												}
228 | 												if (session.id === props.session?.session.id)
229 | 													router.refresh();
230 | 												setIsTerminating(undefined);
231 | 											}}
232 | 										>
233 | 											{isTerminating === session.id ? (
234 | 												<Loader2 size={15} className="animate-spin" />
235 | 											) : session.id === props.session?.session.id ? (
236 | 												"Sign Out"
237 | 											) : (
238 | 												"Terminate"
239 | 											)}
240 | 										</button>
241 | 									</div>
242 | 								</div>
243 | 							);
244 | 						})}
245 | 				</div>
246 | 				<div className="border-y py-4 flex items-center flex-wrap justify-between gap-2">
247 | 					<div className="flex flex-col gap-2">
248 | 						<p className="text-sm">Passkeys</p>
249 | 						<div className="flex gap-2 flex-wrap">
250 | 							<AddPasskey />
251 | 							<ListPasskeys />
252 | 						</div>
253 | 					</div>
254 | 					<div className="flex flex-col gap-2">
255 | 						<p className="text-sm">Two Factor</p>
256 | 						<div className="flex gap-2">
257 | 							{!!session?.user.twoFactorEnabled && (
258 | 								<Dialog>
259 | 									<DialogTrigger asChild>
260 | 										<Button variant="outline" className="gap-2">
261 | 											<QrCode size={16} />
262 | 											<span className="md:text-sm text-xs">Scan QR Code</span>
263 | 										</Button>
264 | 									</DialogTrigger>
265 | 									<DialogContent className="sm:max-w-[425px] w-11/12">
266 | 										<DialogHeader>
267 | 											<DialogTitle>Scan QR Code</DialogTitle>
268 | 											<DialogDescription>
269 | 												Scan the QR code with your TOTP app
270 | 											</DialogDescription>
271 | 										</DialogHeader>
272 | 
273 | 										{twoFactorVerifyURI ? (
274 | 											<>
275 | 												<div className="flex items-center justify-center">
276 | 													<QRCode value={twoFactorVerifyURI} />
277 | 												</div>
278 | 												<div className="flex gap-2 items-center justify-center">
279 | 													<p className="text-sm text-muted-foreground">
280 | 														Copy URI to clipboard
281 | 													</p>
282 | 													<CopyButton textToCopy={twoFactorVerifyURI} />
283 | 												</div>
284 | 											</>
285 | 										) : (
286 | 											<div className="flex flex-col gap-2">
287 | 												<PasswordInput
288 | 													value={twoFaPassword}
289 | 													onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
290 | 														setTwoFaPassword(e.target.value)
291 | 													}
292 | 													placeholder="Enter Password"
293 | 												/>
294 | 												<Button
295 | 													onClick={async () => {
296 | 														if (twoFaPassword.length < 8) {
297 | 															toast.error(
298 | 																"Password must be at least 8 characters",
299 | 															);
300 | 															return;
301 | 														}
302 | 														await client.twoFactor.getTotpUri(
303 | 															{
304 | 																password: twoFaPassword,
305 | 															},
306 | 															{
307 | 																onSuccess(context) {
308 | 																	setTwoFactorVerifyURI(context.data.totpURI);
309 | 																},
310 | 															},
311 | 														);
312 | 														setTwoFaPassword("");
313 | 													}}
314 | 												>
315 | 													Show QR Code
316 | 												</Button>
317 | 											</div>
318 | 										)}
319 | 									</DialogContent>
320 | 								</Dialog>
321 | 							)}
322 | 							<Dialog open={twoFactorDialog} onOpenChange={setTwoFactorDialog}>
323 | 								<DialogTrigger asChild>
324 | 									<Button
325 | 										variant={
326 | 											session?.user.twoFactorEnabled ? "destructive" : "outline"
327 | 										}
328 | 										className="gap-2"
329 | 									>
330 | 										{session?.user.twoFactorEnabled ? (
331 | 											<ShieldOff size={16} />
332 | 										) : (
333 | 											<ShieldCheck size={16} />
334 | 										)}
335 | 										<span className="md:text-sm text-xs">
336 | 											{session?.user.twoFactorEnabled
337 | 												? "Disable 2FA"
338 | 												: "Enable 2FA"}
339 | 										</span>
340 | 									</Button>
341 | 								</DialogTrigger>
342 | 								<DialogContent className="sm:max-w-[425px] w-11/12">
343 | 									<DialogHeader>
344 | 										<DialogTitle>
345 | 											{session?.user.twoFactorEnabled
346 | 												? "Disable 2FA"
347 | 												: "Enable 2FA"}
348 | 										</DialogTitle>
349 | 										<DialogDescription>
350 | 											{session?.user.twoFactorEnabled
351 | 												? "Disable the second factor authentication from your account"
352 | 												: "Enable 2FA to secure your account"}
353 | 										</DialogDescription>
354 | 									</DialogHeader>
355 | 
356 | 									{twoFactorVerifyURI ? (
357 | 										<div className="flex flex-col gap-2">
358 | 											<div className="flex items-center justify-center">
359 | 												<QRCode value={twoFactorVerifyURI} />
360 | 											</div>
361 | 											<Label htmlFor="password">
362 | 												Scan the QR code with your TOTP app
363 | 											</Label>
364 | 											<Input
365 | 												value={twoFaPassword}
366 | 												onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
367 | 													setTwoFaPassword(e.target.value)
368 | 												}
369 | 												placeholder="Enter OTP"
370 | 											/>
371 | 										</div>
372 | 									) : (
373 | 										<div className="flex flex-col gap-2">
374 | 											<Label htmlFor="password">Password</Label>
375 | 											<PasswordInput
376 | 												id="password"
377 | 												placeholder="Password"
378 | 												value={twoFaPassword}
379 | 												onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
380 | 													setTwoFaPassword(e.target.value)
381 | 												}
382 | 											/>
383 | 										</div>
384 | 									)}
385 | 									<DialogFooter>
386 | 										<Button
387 | 											disabled={isPendingTwoFa}
388 | 											onClick={async () => {
389 | 												if (twoFaPassword.length < 8 && !twoFactorVerifyURI) {
390 | 													toast.error("Password must be at least 8 characters");
391 | 													return;
392 | 												}
393 | 												setIsPendingTwoFa(true);
394 | 												if (session?.user.twoFactorEnabled) {
395 | 													const res = await client.twoFactor.disable({
396 | 														password: twoFaPassword,
397 | 														fetchOptions: {
398 | 															onError(context) {
399 | 																toast.error(context.error.message);
400 | 															},
401 | 															onSuccess() {
402 | 																toast("2FA disabled successfully");
403 | 																setTwoFactorDialog(false);
404 | 															},
405 | 														},
406 | 													});
407 | 												} else {
408 | 													if (twoFactorVerifyURI) {
409 | 														await client.twoFactor.verifyTotp({
410 | 															code: twoFaPassword,
411 | 															fetchOptions: {
412 | 																onError(context) {
413 | 																	setIsPendingTwoFa(false);
414 | 																	setTwoFaPassword("");
415 | 																	toast.error(context.error.message);
416 | 																},
417 | 																onSuccess() {
418 | 																	toast("2FA enabled successfully");
419 | 																	setTwoFactorVerifyURI("");
420 | 																	setIsPendingTwoFa(false);
421 | 																	setTwoFaPassword("");
422 | 																	setTwoFactorDialog(false);
423 | 																},
424 | 															},
425 | 														});
426 | 														return;
427 | 													}
428 | 													const res = await client.twoFactor.enable({
429 | 														password: twoFaPassword,
430 | 														fetchOptions: {
431 | 															onError(context) {
432 | 																toast.error(context.error.message);
433 | 															},
434 | 															onSuccess(ctx) {
435 | 																setTwoFactorVerifyURI(ctx.data.totpURI);
436 | 																// toast.success("2FA enabled successfully");
437 | 																// setTwoFactorDialog(false);
438 | 															},
439 | 														},
440 | 													});
441 | 												}
442 | 												setIsPendingTwoFa(false);
443 | 												setTwoFaPassword("");
444 | 											}}
445 | 										>
446 | 											{isPendingTwoFa ? (
447 | 												<Loader2 size={15} className="animate-spin" />
448 | 											) : session?.user.twoFactorEnabled ? (
449 | 												"Disable 2FA"
450 | 											) : (
451 | 												"Enable 2FA"
452 | 											)}
453 | 										</Button>
454 | 									</DialogFooter>
455 | 								</DialogContent>
456 | 							</Dialog>
457 | 						</div>
458 | 					</div>
459 | 				</div>
460 | 			</CardContent>
461 | 			<CardFooter className="gap-2 justify-between items-center">
462 | 				<ChangePassword />
463 | 				{session?.session.impersonatedBy ? (
464 | 					<Button
465 | 						className="gap-2 z-10"
466 | 						variant="secondary"
467 | 						onClick={async () => {
468 | 							setIsSignOut(true);
469 | 							await client.admin.stopImpersonating();
470 | 							setIsSignOut(false);
471 | 							toast.info("Impersonation stopped successfully");
472 | 							router.push("/admin");
473 | 						}}
474 | 						disabled={isSignOut}
475 | 					>
476 | 						<span className="text-sm">
477 | 							{isSignOut ? (
478 | 								<Loader2 size={15} className="animate-spin" />
479 | 							) : (
480 | 								<div className="flex items-center gap-2">
481 | 									<StopCircle size={16} color="red" />
482 | 									Stop Impersonation
483 | 								</div>
484 | 							)}
485 | 						</span>
486 | 					</Button>
487 | 				) : (
488 | 					<Button
489 | 						className="gap-2 z-10"
490 | 						variant="secondary"
491 | 						onClick={async () => {
492 | 							setIsSignOut(true);
493 | 							await signOut({
494 | 								fetchOptions: {
495 | 									onSuccess() {
496 | 										router.push("/");
497 | 									},
498 | 								},
499 | 							});
500 | 							setIsSignOut(false);
501 | 						}}
502 | 						disabled={isSignOut}
503 | 					>
504 | 						<span className="text-sm">
505 | 							{isSignOut ? (
506 | 								<Loader2 size={15} className="animate-spin" />
507 | 							) : (
508 | 								<div className="flex items-center gap-2">
509 | 									<LogOut size={16} />
510 | 									Sign Out
511 | 								</div>
512 | 							)}
513 | 						</span>
514 | 					</Button>
515 | 				)}
516 | 			</CardFooter>
517 | 		</Card>
518 | 	);
519 | }
520 | 
521 | async function convertImageToBase64(file: File): Promise<string> {
522 | 	return new Promise((resolve, reject) => {
523 | 		const reader = new FileReader();
524 | 		reader.onloadend = () => resolve(reader.result as string);
525 | 		reader.onerror = reject;
526 | 		reader.readAsDataURL(file);
527 | 	});
528 | }
529 | 
530 | function ChangePassword() {
531 | 	const [currentPassword, setCurrentPassword] = useState<string>("");
532 | 	const [newPassword, setNewPassword] = useState<string>("");
533 | 	const [confirmPassword, setConfirmPassword] = useState<string>("");
534 | 	const [loading, setLoading] = useState<boolean>(false);
535 | 	const [open, setOpen] = useState<boolean>(false);
536 | 	const [signOutDevices, setSignOutDevices] = useState<boolean>(false);
537 | 	return (
538 | 		<Dialog open={open} onOpenChange={setOpen}>
539 | 			<DialogTrigger asChild>
540 | 				<Button className="gap-2 z-10" variant="outline" size="sm">
541 | 					<svg
542 | 						xmlns="http://www.w3.org/2000/svg"
543 | 						width="1em"
544 | 						height="1em"
545 | 						viewBox="0 0 24 24"
546 | 					>
547 | 						<path
548 | 							fill="currentColor"
549 | 							d="M2.5 18.5v-1h19v1zm.535-5.973l-.762-.442l.965-1.693h-1.93v-.884h1.93l-.965-1.642l.762-.443L4 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L4 10.835zm8 0l-.762-.442l.966-1.693H9.308v-.884h1.93l-.965-1.642l.762-.443L12 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L12 10.835zm8 0l-.762-.442l.966-1.693h-1.931v-.884h1.93l-.965-1.642l.762-.443L20 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L20 10.835z"
550 | 						></path>
551 | 					</svg>
552 | 					<span className="text-sm text-muted-foreground">Change Password</span>
553 | 				</Button>
554 | 			</DialogTrigger>
555 | 			<DialogContent className="sm:max-w-[425px] w-11/12">
556 | 				<DialogHeader>
557 | 					<DialogTitle>Change Password</DialogTitle>
558 | 					<DialogDescription>Change your password</DialogDescription>
559 | 				</DialogHeader>
560 | 				<div className="grid gap-2">
561 | 					<Label htmlFor="current-password">Current Password</Label>
562 | 					<PasswordInput
563 | 						id="current-password"
564 | 						value={currentPassword}
565 | 						onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
566 | 							setCurrentPassword(e.target.value)
567 | 						}
568 | 						autoComplete="new-password"
569 | 						placeholder="Password"
570 | 					/>
571 | 					<Label htmlFor="new-password">New Password</Label>
572 | 					<PasswordInput
573 | 						value={newPassword}
574 | 						onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
575 | 							setNewPassword(e.target.value)
576 | 						}
577 | 						autoComplete="new-password"
578 | 						placeholder="New Password"
579 | 					/>
580 | 					<Label htmlFor="password">Confirm Password</Label>
581 | 					<PasswordInput
582 | 						value={confirmPassword}
583 | 						onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
584 | 							setConfirmPassword(e.target.value)
585 | 						}
586 | 						autoComplete="new-password"
587 | 						placeholder="Confirm Password"
588 | 					/>
589 | 					<div className="flex gap-2 items-center">
590 | 						<Checkbox
591 | 							onCheckedChange={(checked) =>
592 | 								checked ? setSignOutDevices(true) : setSignOutDevices(false)
593 | 							}
594 | 						/>
595 | 						<p className="text-sm">Sign out from other devices</p>
596 | 					</div>
597 | 				</div>
598 | 				<DialogFooter>
599 | 					<Button
600 | 						onClick={async () => {
601 | 							if (newPassword !== confirmPassword) {
602 | 								toast.error("Passwords do not match");
603 | 								return;
604 | 							}
605 | 							if (newPassword.length < 8) {
606 | 								toast.error("Password must be at least 8 characters");
607 | 								return;
608 | 							}
609 | 							setLoading(true);
610 | 							const res = await client.changePassword({
611 | 								newPassword: newPassword,
612 | 								currentPassword: currentPassword,
613 | 								revokeOtherSessions: signOutDevices,
614 | 							});
615 | 							setLoading(false);
616 | 							if (res.error) {
617 | 								toast.error(
618 | 									res.error.message ||
619 | 										"Couldn't change your password! Make sure it's correct",
620 | 								);
621 | 							} else {
622 | 								setOpen(false);
623 | 								toast.success("Password changed successfully");
624 | 								setCurrentPassword("");
625 | 								setNewPassword("");
626 | 								setConfirmPassword("");
627 | 							}
628 | 						}}
629 | 					>
630 | 						{loading ? (
631 | 							<Loader2 size={15} className="animate-spin" />
632 | 						) : (
633 | 							"Change Password"
634 | 						)}
635 | 					</Button>
636 | 				</DialogFooter>
637 | 			</DialogContent>
638 | 		</Dialog>
639 | 	);
640 | }
641 | 
642 | function EditUserDialog() {
643 | 	const { data, isPending, error } = useSession();
644 | 	const [name, setName] = useState<string>();
645 | 	const router = useRouter();
646 | 	const [image, setImage] = useState<File | null>(null);
647 | 	const [imagePreview, setImagePreview] = useState<string | null>(null);
648 | 	const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
649 | 		const file = e.target.files?.[0];
650 | 		if (file) {
651 | 			setImage(file);
652 | 			const reader = new FileReader();
653 | 			reader.onloadend = () => {
654 | 				setImagePreview(reader.result as string);
655 | 			};
656 | 			reader.readAsDataURL(file);
657 | 		}
658 | 	};
659 | 	const [open, setOpen] = useState<boolean>(false);
660 | 	const [isLoading, startTransition] = useTransition();
661 | 	return (
662 | 		<Dialog open={open} onOpenChange={setOpen}>
663 | 			<DialogTrigger asChild>
664 | 				<Button size="sm" className="gap-2" variant="secondary">
665 | 					<Edit size={13} />
666 | 					Edit User
667 | 				</Button>
668 | 			</DialogTrigger>
669 | 			<DialogContent className="sm:max-w-[425px] w-11/12">
670 | 				<DialogHeader>
671 | 					<DialogTitle>Edit User</DialogTitle>
672 | 					<DialogDescription>Edit user information</DialogDescription>
673 | 				</DialogHeader>
674 | 				<div className="grid gap-2">
675 | 					<Label htmlFor="name">Full Name</Label>
676 | 					<Input
677 | 						id="name"
678 | 						type="name"
679 | 						placeholder={data?.user.name}
680 | 						required
681 | 						onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
682 | 							setName(e.target.value);
683 | 						}}
684 | 					/>
685 | 					<div className="grid gap-2">
686 | 						<Label htmlFor="image">Profile Image</Label>
687 | 						<div className="flex items-end gap-4">
688 | 							{imagePreview && (
689 | 								<div className="relative w-16 h-16 rounded-sm overflow-hidden">
690 | 									<Image
691 | 										src={imagePreview}
692 | 										alt="Profile preview"
693 | 										layout="fill"
694 | 										objectFit="cover"
695 | 									/>
696 | 								</div>
697 | 							)}
698 | 							<div className="flex items-center gap-2 w-full">
699 | 								<Input
700 | 									id="image"
701 | 									type="file"
702 | 									accept="image/*"
703 | 									onChange={handleImageChange}
704 | 									className="w-full text-muted-foreground"
705 | 								/>
706 | 								{imagePreview && (
707 | 									<X
708 | 										className="cursor-pointer"
709 | 										onClick={() => {
710 | 											setImage(null);
711 | 											setImagePreview(null);
712 | 										}}
713 | 									/>
714 | 								)}
715 | 							</div>
716 | 						</div>
717 | 					</div>
718 | 				</div>
719 | 				<DialogFooter>
720 | 					<Button
721 | 						disabled={isLoading}
722 | 						onClick={async () => {
723 | 							startTransition(async () => {
724 | 								await client.updateUser({
725 | 									image: image ? await convertImageToBase64(image) : undefined,
726 | 									name: name ? name : undefined,
727 | 									fetchOptions: {
728 | 										onSuccess: () => {
729 | 											toast.success("User updated successfully");
730 | 										},
731 | 										onError: (error) => {
732 | 											toast.error(error.error.message);
733 | 										},
734 | 									},
735 | 								});
736 | 								startTransition(() => {
737 | 									setName("");
738 | 									router.refresh();
739 | 									setImage(null);
740 | 									setImagePreview(null);
741 | 									setOpen(false);
742 | 								});
743 | 							});
744 | 						}}
745 | 					>
746 | 						{isLoading ? (
747 | 							<Loader2 size={15} className="animate-spin" />
748 | 						) : (
749 | 							"Update"
750 | 						)}
751 | 					</Button>
752 | 				</DialogFooter>
753 | 			</DialogContent>
754 | 		</Dialog>
755 | 	);
756 | }
757 | 
758 | function AddPasskey() {
759 | 	const [isOpen, setIsOpen] = useState(false);
760 | 	const [passkeyName, setPasskeyName] = useState("");
761 | 	const [isLoading, setIsLoading] = useState(false);
762 | 
763 | 	const handleAddPasskey = async () => {
764 | 		if (!passkeyName) {
765 | 			toast.error("Passkey name is required");
766 | 			return;
767 | 		}
768 | 		setIsLoading(true);
769 | 		const res = await client.passkey.addPasskey({
770 | 			name: passkeyName,
771 | 		});
772 | 		if (res?.error) {
773 | 			toast.error(res?.error.message);
774 | 		} else {
775 | 			setIsOpen(false);
776 | 			toast.success("Passkey added successfully. You can now use it to login.");
777 | 		}
778 | 		setIsLoading(false);
779 | 	};
780 | 	return (
781 | 		<Dialog open={isOpen} onOpenChange={setIsOpen}>
782 | 			<DialogTrigger asChild>
783 | 				<Button variant="outline" className="gap-2 text-xs md:text-sm">
784 | 					<Plus size={15} />
785 | 					Add New Passkey
786 | 				</Button>
787 | 			</DialogTrigger>
788 | 			<DialogContent className="sm:max-w-[425px] w-11/12">
789 | 				<DialogHeader>
790 | 					<DialogTitle>Add New Passkey</DialogTitle>
791 | 					<DialogDescription>
792 | 						Create a new passkey to securely access your account without a
793 | 						password.
794 | 					</DialogDescription>
795 | 				</DialogHeader>
796 | 				<div className="grid gap-2">
797 | 					<Label htmlFor="passkey-name">Passkey Name</Label>
798 | 					<Input
799 | 						id="passkey-name"
800 | 						value={passkeyName}
801 | 						onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
802 | 							setPasskeyName(e.target.value)
803 | 						}
804 | 					/>
805 | 				</div>
806 | 				<DialogFooter>
807 | 					<Button
808 | 						disabled={isLoading}
809 | 						type="submit"
810 | 						onClick={handleAddPasskey}
811 | 						className="w-full"
812 | 					>
813 | 						{isLoading ? (
814 | 							<Loader2 size={15} className="animate-spin" />
815 | 						) : (
816 | 							<>
817 | 								<Fingerprint className="mr-2 h-4 w-4" />
818 | 								Create Passkey
819 | 							</>
820 | 						)}
821 | 					</Button>
822 | 				</DialogFooter>
823 | 			</DialogContent>
824 | 		</Dialog>
825 | 	);
826 | }
827 | 
828 | function ListPasskeys() {
829 | 	const { data } = client.useListPasskeys();
830 | 	const [isOpen, setIsOpen] = useState(false);
831 | 	const [passkeyName, setPasskeyName] = useState("");
832 | 
833 | 	const handleAddPasskey = async () => {
834 | 		if (!passkeyName) {
835 | 			toast.error("Passkey name is required");
836 | 			return;
837 | 		}
838 | 		setIsLoading(true);
839 | 		const res = await client.passkey.addPasskey({
840 | 			name: passkeyName,
841 | 		});
842 | 		setIsLoading(false);
843 | 		if (res?.error) {
844 | 			toast.error(res?.error.message);
845 | 		} else {
846 | 			toast.success("Passkey added successfully. You can now use it to login.");
847 | 		}
848 | 	};
849 | 	const [isLoading, setIsLoading] = useState(false);
850 | 	const [isDeletePasskey, setIsDeletePasskey] = useState<boolean>(false);
851 | 	return (
852 | 		<Dialog open={isOpen} onOpenChange={setIsOpen}>
853 | 			<DialogTrigger asChild>
854 | 				<Button variant="outline" className="text-xs md:text-sm">
855 | 					<Fingerprint className="mr-2 h-4 w-4" />
856 | 					<span>Passkeys {data?.length ? `[${data?.length}]` : ""}</span>
857 | 				</Button>
858 | 			</DialogTrigger>
859 | 			<DialogContent className="sm:max-w-[425px] w-11/12">
860 | 				<DialogHeader>
861 | 					<DialogTitle>Passkeys</DialogTitle>
862 | 					<DialogDescription>List of passkeys</DialogDescription>
863 | 				</DialogHeader>
864 | 				{data?.length ? (
865 | 					<Table>
866 | 						<TableHeader>
867 | 							<TableRow>
868 | 								<TableHead>Name</TableHead>
869 | 							</TableRow>
870 | 						</TableHeader>
871 | 						<TableBody>
872 | 							{data.map((passkey) => (
873 | 								<TableRow
874 | 									key={passkey.id}
875 | 									className="flex  justify-between items-center"
876 | 								>
877 | 									<TableCell>{passkey.name || "My Passkey"}</TableCell>
878 | 									<TableCell className="text-right">
879 | 										<button
880 | 											onClick={async () => {
881 | 												const res = await client.passkey.deletePasskey({
882 | 													id: passkey.id,
883 | 													fetchOptions: {
884 | 														onRequest: () => {
885 | 															setIsDeletePasskey(true);
886 | 														},
887 | 														onSuccess: () => {
888 | 															toast("Passkey deleted successfully");
889 | 															setIsDeletePasskey(false);
890 | 														},
891 | 														onError: (error) => {
892 | 															toast.error(error.error.message);
893 | 															setIsDeletePasskey(false);
894 | 														},
895 | 													},
896 | 												});
897 | 											}}
898 | 										>
899 | 											{isDeletePasskey ? (
900 | 												<Loader2 size={15} className="animate-spin" />
901 | 											) : (
902 | 												<Trash
903 | 													size={15}
904 | 													className="cursor-pointer text-red-600"
905 | 												/>
906 | 											)}
907 | 										</button>
908 | 									</TableCell>
909 | 								</TableRow>
910 | 							))}
911 | 						</TableBody>
912 | 					</Table>
913 | 				) : (
914 | 					<p className="text-sm text-muted-foreground">No passkeys found</p>
915 | 				)}
916 | 				{!data?.length && (
917 | 					<div className="flex flex-col gap-2">
918 | 						<div className="flex flex-col gap-2">
919 | 							<Label htmlFor="passkey-name" className="text-sm">
920 | 								New Passkey
921 | 							</Label>
922 | 							<Input
923 | 								id="passkey-name"
924 | 								value={passkeyName}
925 | 								onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
926 | 									setPasskeyName(e.target.value)
927 | 								}
928 | 								placeholder="My Passkey"
929 | 							/>
930 | 						</div>
931 | 						<Button type="submit" onClick={handleAddPasskey} className="w-full">
932 | 							{isLoading ? (
933 | 								<Loader2 size={15} className="animate-spin" />
934 | 							) : (
935 | 								<>
936 | 									<Fingerprint className="mr-2 h-4 w-4" />
937 | 									Create Passkey
938 | 								</>
939 | 							)}
940 | 						</Button>
941 | 					</div>
942 | 				)}
943 | 				<DialogFooter>
944 | 					<Button onClick={() => setIsOpen(false)}>Close</Button>
945 | 				</DialogFooter>
946 | 			</DialogContent>
947 | 		</Dialog>
948 | 	);
949 | }
950 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/mcp/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import * as z from "zod";
   2 | import {
   3 | 	createAuthEndpoint,
   4 | 	createAuthMiddleware,
   5 | } from "@better-auth/core/api";
   6 | import type { BetterAuthPlugin, BetterAuthOptions } from "@better-auth/core";
   7 | import {
   8 | 	oidcProvider,
   9 | 	type Client,
  10 | 	type CodeVerificationValue,
  11 | 	type OAuthAccessToken,
  12 | 	type OIDCMetadata,
  13 | 	type OIDCOptions,
  14 | } from "../oidc-provider";
  15 | import { APIError, getSessionFromCtx } from "../../api";
  16 | import { base64 } from "@better-auth/utils/base64";
  17 | import { generateRandomString } from "../../crypto";
  18 | import { createHash } from "@better-auth/utils/hash";
  19 | import { getWebcryptoSubtle } from "@better-auth/utils";
  20 | import { SignJWT } from "jose";
  21 | import { parseSetCookieHeader } from "../../cookies";
  22 | import { schema } from "../oidc-provider/schema";
  23 | import { authorizeMCPOAuth } from "./authorize";
  24 | import { getBaseURL } from "../../utils/url";
  25 | import { isProduction } from "@better-auth/core/env";
  26 | import { logger } from "@better-auth/core/env";
  27 | import type { GenericEndpointContext } from "@better-auth/core";
  28 | 
  29 | interface MCPOptions {
  30 | 	loginPage: string;
  31 | 	resource?: string;
  32 | 	oidcConfig?: OIDCOptions;
  33 | }
  34 | 
  35 | export const getMCPProviderMetadata = (
  36 | 	ctx: GenericEndpointContext,
  37 | 	options?: OIDCOptions,
  38 | ): OIDCMetadata => {
  39 | 	const issuer = ctx.context.options.baseURL as string;
  40 | 	const baseURL = ctx.context.baseURL;
  41 | 	if (!issuer || !baseURL) {
  42 | 		throw new APIError("INTERNAL_SERVER_ERROR", {
  43 | 			error: "invalid_issuer",
  44 | 			error_description:
  45 | 				"issuer or baseURL is not set. If you're the app developer, please make sure to set the `baseURL` in your auth config.",
  46 | 		});
  47 | 	}
  48 | 	return {
  49 | 		issuer,
  50 | 		authorization_endpoint: `${baseURL}/mcp/authorize`,
  51 | 		token_endpoint: `${baseURL}/mcp/token`,
  52 | 		userinfo_endpoint: `${baseURL}/mcp/userinfo`,
  53 | 		jwks_uri: `${baseURL}/mcp/jwks`,
  54 | 		registration_endpoint: `${baseURL}/mcp/register`,
  55 | 		scopes_supported: ["openid", "profile", "email", "offline_access"],
  56 | 		response_types_supported: ["code"],
  57 | 		response_modes_supported: ["query"],
  58 | 		grant_types_supported: ["authorization_code", "refresh_token"],
  59 | 		acr_values_supported: [
  60 | 			"urn:mace:incommon:iap:silver",
  61 | 			"urn:mace:incommon:iap:bronze",
  62 | 		],
  63 | 		subject_types_supported: ["public"],
  64 | 		id_token_signing_alg_values_supported: ["RS256", "none"],
  65 | 		token_endpoint_auth_methods_supported: [
  66 | 			"client_secret_basic",
  67 | 			"client_secret_post",
  68 | 			"none",
  69 | 		],
  70 | 		code_challenge_methods_supported: ["S256"],
  71 | 		claims_supported: [
  72 | 			"sub",
  73 | 			"iss",
  74 | 			"aud",
  75 | 			"exp",
  76 | 			"nbf",
  77 | 			"iat",
  78 | 			"jti",
  79 | 			"email",
  80 | 			"email_verified",
  81 | 			"name",
  82 | 		],
  83 | 		...options?.metadata,
  84 | 	};
  85 | };
  86 | 
  87 | export const getMCPProtectedResourceMetadata = (
  88 | 	ctx: GenericEndpointContext,
  89 | 	options?: MCPOptions,
  90 | ) => {
  91 | 	const baseURL = ctx.context.baseURL;
  92 | 
  93 | 	return {
  94 | 		resource: options?.resource ?? new URL(baseURL).origin,
  95 | 		authorization_servers: [baseURL],
  96 | 		jwks_uri: options?.oidcConfig?.metadata?.jwks_uri ?? `${baseURL}/mcp/jwks`,
  97 | 		scopes_supported: options?.oidcConfig?.metadata?.scopes_supported ?? [
  98 | 			"openid",
  99 | 			"profile",
 100 | 			"email",
 101 | 			"offline_access",
 102 | 		],
 103 | 		bearer_methods_supported: ["header"],
 104 | 		resource_signing_alg_values_supported: ["RS256", "none"],
 105 | 	};
 106 | };
 107 | 
 108 | export const mcp = (options: MCPOptions) => {
 109 | 	const opts = {
 110 | 		codeExpiresIn: 600,
 111 | 		defaultScope: "openid",
 112 | 		accessTokenExpiresIn: 3600,
 113 | 		refreshTokenExpiresIn: 604800,
 114 | 		allowPlainCodeChallengeMethod: true,
 115 | 		...options.oidcConfig,
 116 | 		loginPage: options.loginPage,
 117 | 		scopes: [
 118 | 			"openid",
 119 | 			"profile",
 120 | 			"email",
 121 | 			"offline_access",
 122 | 			...(options.oidcConfig?.scopes || []),
 123 | 		],
 124 | 	};
 125 | 	const modelName = {
 126 | 		oauthClient: "oauthApplication",
 127 | 		oauthAccessToken: "oauthAccessToken",
 128 | 		oauthConsent: "oauthConsent",
 129 | 	};
 130 | 	const provider = oidcProvider(opts);
 131 | 	return {
 132 | 		id: "mcp",
 133 | 		hooks: {
 134 | 			after: [
 135 | 				{
 136 | 					matcher() {
 137 | 						return true;
 138 | 					},
 139 | 					handler: createAuthMiddleware(async (ctx) => {
 140 | 						const cookie = await ctx.getSignedCookie(
 141 | 							"oidc_login_prompt",
 142 | 							ctx.context.secret,
 143 | 						);
 144 | 						const cookieName = ctx.context.authCookies.sessionToken.name;
 145 | 						const parsedSetCookieHeader = parseSetCookieHeader(
 146 | 							ctx.context.responseHeaders?.get("set-cookie") || "",
 147 | 						);
 148 | 						const hasSessionToken = parsedSetCookieHeader.has(cookieName);
 149 | 						if (!cookie || !hasSessionToken) {
 150 | 							return;
 151 | 						}
 152 | 						ctx.setCookie("oidc_login_prompt", "", {
 153 | 							maxAge: 0,
 154 | 						});
 155 | 						const sessionCookie = parsedSetCookieHeader.get(cookieName)?.value;
 156 | 						const sessionToken = sessionCookie?.split(".")[0]!;
 157 | 						if (!sessionToken) {
 158 | 							return;
 159 | 						}
 160 | 						const session =
 161 | 							await ctx.context.internalAdapter.findSession(sessionToken);
 162 | 						if (!session) {
 163 | 							return;
 164 | 						}
 165 | 						ctx.query = JSON.parse(cookie);
 166 | 						ctx.query!.prompt = "consent";
 167 | 						ctx.context.session = session;
 168 | 						const response = await authorizeMCPOAuth(ctx, opts);
 169 | 						return response;
 170 | 					}),
 171 | 				},
 172 | 			],
 173 | 		},
 174 | 		endpoints: {
 175 | 			getMcpOAuthConfig: createAuthEndpoint(
 176 | 				"/.well-known/oauth-authorization-server",
 177 | 				{
 178 | 					method: "GET",
 179 | 					metadata: {
 180 | 						client: false,
 181 | 					},
 182 | 				},
 183 | 				async (c) => {
 184 | 					try {
 185 | 						const metadata = getMCPProviderMetadata(c, options);
 186 | 						return c.json(metadata);
 187 | 					} catch (e) {
 188 | 						console.log(e);
 189 | 						return c.json(null);
 190 | 					}
 191 | 				},
 192 | 			),
 193 | 			getMCPProtectedResource: createAuthEndpoint(
 194 | 				"/.well-known/oauth-protected-resource",
 195 | 				{
 196 | 					method: "GET",
 197 | 					metadata: {
 198 | 						client: false,
 199 | 					},
 200 | 				},
 201 | 				async (c) => {
 202 | 					const metadata = getMCPProtectedResourceMetadata(c, options);
 203 | 					return c.json(metadata);
 204 | 				},
 205 | 			),
 206 | 			mcpOAuthAuthorize: createAuthEndpoint(
 207 | 				"/mcp/authorize",
 208 | 				{
 209 | 					method: "GET",
 210 | 					query: z.record(z.string(), z.any()),
 211 | 					metadata: {
 212 | 						openapi: {
 213 | 							description: "Authorize an OAuth2 request using MCP",
 214 | 							responses: {
 215 | 								"200": {
 216 | 									description: "Authorization response generated successfully",
 217 | 									content: {
 218 | 										"application/json": {
 219 | 											schema: {
 220 | 												type: "object",
 221 | 												additionalProperties: true,
 222 | 												description:
 223 | 													"Authorization response, contents depend on the authorize function implementation",
 224 | 											},
 225 | 										},
 226 | 									},
 227 | 								},
 228 | 							},
 229 | 						},
 230 | 					},
 231 | 				},
 232 | 				async (ctx) => {
 233 | 					return authorizeMCPOAuth(ctx, opts);
 234 | 				},
 235 | 			),
 236 | 			mcpOAuthToken: createAuthEndpoint(
 237 | 				"/mcp/token",
 238 | 				{
 239 | 					method: "POST",
 240 | 					body: z.record(z.any(), z.any()),
 241 | 					metadata: {
 242 | 						isAction: false,
 243 | 					},
 244 | 				},
 245 | 				async (ctx) => {
 246 | 					//cors
 247 | 					ctx.setHeader("Access-Control-Allow-Origin", "*");
 248 | 					ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
 249 | 					ctx.setHeader(
 250 | 						"Access-Control-Allow-Headers",
 251 | 						"Content-Type, Authorization",
 252 | 					);
 253 | 					ctx.setHeader("Access-Control-Max-Age", "86400");
 254 | 
 255 | 					let { body } = ctx;
 256 | 					if (!body) {
 257 | 						throw ctx.error("BAD_REQUEST", {
 258 | 							error_description: "request body not found",
 259 | 							error: "invalid_request",
 260 | 						});
 261 | 					}
 262 | 					if (body instanceof FormData) {
 263 | 						body = Object.fromEntries(body.entries());
 264 | 					}
 265 | 					if (!(body instanceof Object)) {
 266 | 						throw new APIError("BAD_REQUEST", {
 267 | 							error_description: "request body is not an object",
 268 | 							error: "invalid_request",
 269 | 						});
 270 | 					}
 271 | 					let { client_id, client_secret } = body;
 272 | 					const authorization =
 273 | 						ctx.request?.headers.get("authorization") || null;
 274 | 					if (
 275 | 						authorization &&
 276 | 						!client_id &&
 277 | 						!client_secret &&
 278 | 						authorization.startsWith("Basic ")
 279 | 					) {
 280 | 						try {
 281 | 							const encoded = authorization.replace("Basic ", "");
 282 | 							const decoded = new TextDecoder().decode(base64.decode(encoded));
 283 | 							if (!decoded.includes(":")) {
 284 | 								throw new APIError("UNAUTHORIZED", {
 285 | 									error_description: "invalid authorization header format",
 286 | 									error: "invalid_client",
 287 | 								});
 288 | 							}
 289 | 							const [id, secret] = decoded.split(":");
 290 | 							if (!id || !secret) {
 291 | 								throw new APIError("UNAUTHORIZED", {
 292 | 									error_description: "invalid authorization header format",
 293 | 									error: "invalid_client",
 294 | 								});
 295 | 							}
 296 | 							client_id = id;
 297 | 							client_secret = secret;
 298 | 						} catch (error) {
 299 | 							throw new APIError("UNAUTHORIZED", {
 300 | 								error_description: "invalid authorization header format",
 301 | 								error: "invalid_client",
 302 | 							});
 303 | 						}
 304 | 					}
 305 | 					const {
 306 | 						grant_type,
 307 | 						code,
 308 | 						redirect_uri,
 309 | 						refresh_token,
 310 | 						code_verifier,
 311 | 					} = body;
 312 | 					if (grant_type === "refresh_token") {
 313 | 						if (!refresh_token) {
 314 | 							throw new APIError("BAD_REQUEST", {
 315 | 								error_description: "refresh_token is required",
 316 | 								error: "invalid_request",
 317 | 							});
 318 | 						}
 319 | 						const token = await ctx.context.adapter.findOne<OAuthAccessToken>({
 320 | 							model: "oauthAccessToken",
 321 | 							where: [
 322 | 								{
 323 | 									field: "refreshToken",
 324 | 									value: refresh_token.toString(),
 325 | 								},
 326 | 							],
 327 | 						});
 328 | 						if (!token) {
 329 | 							throw new APIError("UNAUTHORIZED", {
 330 | 								error_description: "invalid refresh token",
 331 | 								error: "invalid_grant",
 332 | 							});
 333 | 						}
 334 | 						if (token.clientId !== client_id?.toString()) {
 335 | 							throw new APIError("UNAUTHORIZED", {
 336 | 								error_description: "invalid client_id",
 337 | 								error: "invalid_client",
 338 | 							});
 339 | 						}
 340 | 						if (token.refreshTokenExpiresAt < new Date()) {
 341 | 							throw new APIError("UNAUTHORIZED", {
 342 | 								error_description: "refresh token expired",
 343 | 								error: "invalid_grant",
 344 | 							});
 345 | 						}
 346 | 						const accessToken = generateRandomString(32, "a-z", "A-Z");
 347 | 						const newRefreshToken = generateRandomString(32, "a-z", "A-Z");
 348 | 						const accessTokenExpiresAt = new Date(
 349 | 							Date.now() + opts.accessTokenExpiresIn * 1000,
 350 | 						);
 351 | 						const refreshTokenExpiresAt = new Date(
 352 | 							Date.now() + opts.refreshTokenExpiresIn * 1000,
 353 | 						);
 354 | 						await ctx.context.adapter.create({
 355 | 							model: modelName.oauthAccessToken,
 356 | 							data: {
 357 | 								accessToken,
 358 | 								refreshToken: newRefreshToken,
 359 | 								accessTokenExpiresAt,
 360 | 								refreshTokenExpiresAt,
 361 | 								clientId: client_id.toString(),
 362 | 								userId: token.userId,
 363 | 								scopes: token.scopes,
 364 | 								createdAt: new Date(),
 365 | 								updatedAt: new Date(),
 366 | 							},
 367 | 						});
 368 | 						return ctx.json({
 369 | 							access_token: accessToken,
 370 | 							token_type: "bearer",
 371 | 							expires_in: opts.accessTokenExpiresIn,
 372 | 							refresh_token: newRefreshToken,
 373 | 							scope: token.scopes,
 374 | 						});
 375 | 					}
 376 | 
 377 | 					if (!code) {
 378 | 						throw new APIError("BAD_REQUEST", {
 379 | 							error_description: "code is required",
 380 | 							error: "invalid_request",
 381 | 						});
 382 | 					}
 383 | 
 384 | 					if (opts.requirePKCE && !code_verifier) {
 385 | 						throw new APIError("BAD_REQUEST", {
 386 | 							error_description: "code verifier is missing",
 387 | 							error: "invalid_request",
 388 | 						});
 389 | 					}
 390 | 
 391 | 					/**
 392 | 					 * We need to check if the code is valid before we can proceed
 393 | 					 * with the rest of the request.
 394 | 					 */
 395 | 					const verificationValue =
 396 | 						await ctx.context.internalAdapter.findVerificationValue(
 397 | 							code.toString(),
 398 | 						);
 399 | 					if (!verificationValue) {
 400 | 						throw new APIError("UNAUTHORIZED", {
 401 | 							error_description: "invalid code",
 402 | 							error: "invalid_grant",
 403 | 						});
 404 | 					}
 405 | 					if (verificationValue.expiresAt < new Date()) {
 406 | 						throw new APIError("UNAUTHORIZED", {
 407 | 							error_description: "code expired",
 408 | 							error: "invalid_grant",
 409 | 						});
 410 | 					}
 411 | 
 412 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 413 | 						verificationValue.id,
 414 | 					);
 415 | 
 416 | 					if (!client_id) {
 417 | 						throw new APIError("UNAUTHORIZED", {
 418 | 							error_description: "client_id is required",
 419 | 							error: "invalid_client",
 420 | 						});
 421 | 					}
 422 | 					if (!grant_type) {
 423 | 						throw new APIError("BAD_REQUEST", {
 424 | 							error_description: "grant_type is required",
 425 | 							error: "invalid_request",
 426 | 						});
 427 | 					}
 428 | 					if (grant_type !== "authorization_code") {
 429 | 						throw new APIError("BAD_REQUEST", {
 430 | 							error_description: "grant_type must be 'authorization_code'",
 431 | 							error: "unsupported_grant_type",
 432 | 						});
 433 | 					}
 434 | 
 435 | 					if (!redirect_uri) {
 436 | 						throw new APIError("BAD_REQUEST", {
 437 | 							error_description: "redirect_uri is required",
 438 | 							error: "invalid_request",
 439 | 						});
 440 | 					}
 441 | 
 442 | 					const client = await ctx.context.adapter
 443 | 						.findOne<Record<string, any>>({
 444 | 							model: modelName.oauthClient,
 445 | 							where: [{ field: "clientId", value: client_id.toString() }],
 446 | 						})
 447 | 						.then((res) => {
 448 | 							if (!res) {
 449 | 								return null;
 450 | 							}
 451 | 							return {
 452 | 								...res,
 453 | 								redirectURLs: res.redirectURLs.split(","),
 454 | 								metadata: res.metadata ? JSON.parse(res.metadata) : {},
 455 | 							} as Client;
 456 | 						});
 457 | 					if (!client) {
 458 | 						throw new APIError("UNAUTHORIZED", {
 459 | 							error_description: "invalid client_id",
 460 | 							error: "invalid_client",
 461 | 						});
 462 | 					}
 463 | 					if (client.disabled) {
 464 | 						throw new APIError("UNAUTHORIZED", {
 465 | 							error_description: "client is disabled",
 466 | 							error: "invalid_client",
 467 | 						});
 468 | 					}
 469 | 					// For public clients (type: 'public'), validate PKCE instead of client_secret
 470 | 					if (client.type === "public") {
 471 | 						// Public clients must use PKCE
 472 | 						if (!code_verifier) {
 473 | 							throw new APIError("BAD_REQUEST", {
 474 | 								error_description:
 475 | 									"code verifier is required for public clients",
 476 | 								error: "invalid_request",
 477 | 							});
 478 | 						}
 479 | 						// PKCE validation happens later in the flow, so we skip client_secret validation
 480 | 					} else {
 481 | 						// For confidential clients, validate client_secret
 482 | 						if (!client_secret) {
 483 | 							throw new APIError("UNAUTHORIZED", {
 484 | 								error_description:
 485 | 									"client_secret is required for confidential clients",
 486 | 								error: "invalid_client",
 487 | 							});
 488 | 						}
 489 | 						const isValidSecret =
 490 | 							client.clientSecret === client_secret.toString();
 491 | 						if (!isValidSecret) {
 492 | 							throw new APIError("UNAUTHORIZED", {
 493 | 								error_description: "invalid client_secret",
 494 | 								error: "invalid_client",
 495 | 							});
 496 | 						}
 497 | 					}
 498 | 					const value = JSON.parse(
 499 | 						verificationValue.value,
 500 | 					) as CodeVerificationValue;
 501 | 					if (value.clientId !== client_id.toString()) {
 502 | 						throw new APIError("UNAUTHORIZED", {
 503 | 							error_description: "invalid client_id",
 504 | 							error: "invalid_client",
 505 | 						});
 506 | 					}
 507 | 					if (value.redirectURI !== redirect_uri.toString()) {
 508 | 						throw new APIError("UNAUTHORIZED", {
 509 | 							error_description: "invalid redirect_uri",
 510 | 							error: "invalid_client",
 511 | 						});
 512 | 					}
 513 | 					if (value.codeChallenge && !code_verifier) {
 514 | 						throw new APIError("BAD_REQUEST", {
 515 | 							error_description: "code verifier is missing",
 516 | 							error: "invalid_request",
 517 | 						});
 518 | 					}
 519 | 
 520 | 					const challenge =
 521 | 						value.codeChallengeMethod === "plain"
 522 | 							? code_verifier
 523 | 							: await createHash("SHA-256", "base64urlnopad").digest(
 524 | 									code_verifier,
 525 | 								);
 526 | 
 527 | 					if (challenge !== value.codeChallenge) {
 528 | 						throw new APIError("UNAUTHORIZED", {
 529 | 							error_description: "code verification failed",
 530 | 							error: "invalid_request",
 531 | 						});
 532 | 					}
 533 | 
 534 | 					const requestedScopes = value.scope;
 535 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 536 | 						verificationValue.id,
 537 | 					);
 538 | 					const accessToken = generateRandomString(32, "a-z", "A-Z");
 539 | 					const refreshToken = generateRandomString(32, "A-Z", "a-z");
 540 | 					const accessTokenExpiresAt = new Date(
 541 | 						Date.now() + opts.accessTokenExpiresIn * 1000,
 542 | 					);
 543 | 					const refreshTokenExpiresAt = new Date(
 544 | 						Date.now() + opts.refreshTokenExpiresIn * 1000,
 545 | 					);
 546 | 					await ctx.context.adapter.create({
 547 | 						model: modelName.oauthAccessToken,
 548 | 						data: {
 549 | 							accessToken,
 550 | 							refreshToken,
 551 | 							accessTokenExpiresAt,
 552 | 							refreshTokenExpiresAt,
 553 | 							clientId: client_id.toString(),
 554 | 							userId: value.userId,
 555 | 							scopes: requestedScopes.join(" "),
 556 | 							createdAt: new Date(),
 557 | 							updatedAt: new Date(),
 558 | 						},
 559 | 					});
 560 | 					const user = await ctx.context.internalAdapter.findUserById(
 561 | 						value.userId,
 562 | 					);
 563 | 					if (!user) {
 564 | 						throw new APIError("UNAUTHORIZED", {
 565 | 							error_description: "user not found",
 566 | 							error: "invalid_grant",
 567 | 						});
 568 | 					}
 569 | 					let secretKey = {
 570 | 						alg: "HS256",
 571 | 						key: await getWebcryptoSubtle().generateKey(
 572 | 							{
 573 | 								name: "HMAC",
 574 | 								hash: "SHA-256",
 575 | 							},
 576 | 							true,
 577 | 							["sign", "verify"],
 578 | 						),
 579 | 					};
 580 | 					const profile = {
 581 | 						given_name: user.name.split(" ")[0]!,
 582 | 						family_name: user.name.split(" ")[1]!,
 583 | 						name: user.name,
 584 | 						profile: user.image,
 585 | 						updated_at: user.updatedAt.toISOString(),
 586 | 					};
 587 | 					const email = {
 588 | 						email: user.email,
 589 | 						email_verified: user.emailVerified,
 590 | 					};
 591 | 					const userClaims = {
 592 | 						...(requestedScopes.includes("profile") ? profile : {}),
 593 | 						...(requestedScopes.includes("email") ? email : {}),
 594 | 					};
 595 | 
 596 | 					const additionalUserClaims = opts.getAdditionalUserInfoClaim
 597 | 						? await opts.getAdditionalUserInfoClaim(
 598 | 								user,
 599 | 								requestedScopes,
 600 | 								client,
 601 | 							)
 602 | 						: {};
 603 | 
 604 | 					const idToken = await new SignJWT({
 605 | 						sub: user.id,
 606 | 						aud: client_id.toString(),
 607 | 						iat: Date.now(),
 608 | 						auth_time: ctx.context.session
 609 | 							? new Date(ctx.context.session.session.createdAt).getTime()
 610 | 							: undefined,
 611 | 						nonce: value.nonce,
 612 | 						acr: "urn:mace:incommon:iap:silver", // default to silver - ⚠︎ this should be configurable and should be validated against the client's metadata
 613 | 						...userClaims,
 614 | 						...additionalUserClaims,
 615 | 					})
 616 | 						.setProtectedHeader({ alg: secretKey.alg })
 617 | 						.setIssuedAt()
 618 | 						.setExpirationTime(
 619 | 							Math.floor(Date.now() / 1000) + opts.accessTokenExpiresIn,
 620 | 						)
 621 | 						.sign(secretKey.key);
 622 | 					return ctx.json(
 623 | 						{
 624 | 							access_token: accessToken,
 625 | 							token_type: "Bearer",
 626 | 							expires_in: opts.accessTokenExpiresIn,
 627 | 							refresh_token: requestedScopes.includes("offline_access")
 628 | 								? refreshToken
 629 | 								: undefined,
 630 | 							scope: requestedScopes.join(" "),
 631 | 							id_token: requestedScopes.includes("openid")
 632 | 								? idToken
 633 | 								: undefined,
 634 | 						},
 635 | 						{
 636 | 							headers: {
 637 | 								"Cache-Control": "no-store",
 638 | 								Pragma: "no-cache",
 639 | 							},
 640 | 						},
 641 | 					);
 642 | 				},
 643 | 			),
 644 | 			registerMcpClient: createAuthEndpoint(
 645 | 				"/mcp/register",
 646 | 				{
 647 | 					method: "POST",
 648 | 					body: z.object({
 649 | 						redirect_uris: z.array(z.string()),
 650 | 						token_endpoint_auth_method: z
 651 | 							.enum(["none", "client_secret_basic", "client_secret_post"])
 652 | 							.default("client_secret_basic")
 653 | 							.optional(),
 654 | 						grant_types: z
 655 | 							.array(
 656 | 								z.enum([
 657 | 									"authorization_code",
 658 | 									"implicit",
 659 | 									"password",
 660 | 									"client_credentials",
 661 | 									"refresh_token",
 662 | 									"urn:ietf:params:oauth:grant-type:jwt-bearer",
 663 | 									"urn:ietf:params:oauth:grant-type:saml2-bearer",
 664 | 								]),
 665 | 							)
 666 | 							.default(["authorization_code"])
 667 | 							.optional(),
 668 | 						response_types: z
 669 | 							.array(z.enum(["code", "token"]))
 670 | 							.default(["code"])
 671 | 							.optional(),
 672 | 						client_name: z.string().optional(),
 673 | 						client_uri: z.string().optional(),
 674 | 						logo_uri: z.string().optional(),
 675 | 						scope: z.string().optional(),
 676 | 						contacts: z.array(z.string()).optional(),
 677 | 						tos_uri: z.string().optional(),
 678 | 						policy_uri: z.string().optional(),
 679 | 						jwks_uri: z.string().optional(),
 680 | 						jwks: z.record(z.string(), z.any()).optional(),
 681 | 						metadata: z.record(z.any(), z.any()).optional(),
 682 | 						software_id: z.string().optional(),
 683 | 						software_version: z.string().optional(),
 684 | 						software_statement: z.string().optional(),
 685 | 					}),
 686 | 					metadata: {
 687 | 						openapi: {
 688 | 							description: "Register an OAuth2 application",
 689 | 							responses: {
 690 | 								"200": {
 691 | 									description: "OAuth2 application registered successfully",
 692 | 									content: {
 693 | 										"application/json": {
 694 | 											schema: {
 695 | 												type: "object",
 696 | 												properties: {
 697 | 													name: {
 698 | 														type: "string",
 699 | 														description: "Name of the OAuth2 application",
 700 | 													},
 701 | 													icon: {
 702 | 														type: "string",
 703 | 														nullable: true,
 704 | 														description: "Icon URL for the application",
 705 | 													},
 706 | 													metadata: {
 707 | 														type: "object",
 708 | 														additionalProperties: true,
 709 | 														nullable: true,
 710 | 														description:
 711 | 															"Additional metadata for the application",
 712 | 													},
 713 | 													clientId: {
 714 | 														type: "string",
 715 | 														description: "Unique identifier for the client",
 716 | 													},
 717 | 													clientSecret: {
 718 | 														type: "string",
 719 | 														description:
 720 | 															"Secret key for the client. Not included for public clients.",
 721 | 													},
 722 | 													redirectURLs: {
 723 | 														type: "array",
 724 | 														items: { type: "string", format: "uri" },
 725 | 														description: "List of allowed redirect URLs",
 726 | 													},
 727 | 													type: {
 728 | 														type: "string",
 729 | 														description: "Type of the client",
 730 | 														enum: ["web", "public"],
 731 | 													},
 732 | 													authenticationScheme: {
 733 | 														type: "string",
 734 | 														description:
 735 | 															"Authentication scheme used by the client",
 736 | 														enum: ["client_secret", "none"],
 737 | 													},
 738 | 													disabled: {
 739 | 														type: "boolean",
 740 | 														description: "Whether the client is disabled",
 741 | 														enum: [false],
 742 | 													},
 743 | 													userId: {
 744 | 														type: "string",
 745 | 														nullable: true,
 746 | 														description:
 747 | 															"ID of the user who registered the client, null if registered anonymously",
 748 | 													},
 749 | 													createdAt: {
 750 | 														type: "string",
 751 | 														format: "date-time",
 752 | 														description: "Creation timestamp",
 753 | 													},
 754 | 													updatedAt: {
 755 | 														type: "string",
 756 | 														format: "date-time",
 757 | 														description: "Last update timestamp",
 758 | 													},
 759 | 												},
 760 | 												required: [
 761 | 													"name",
 762 | 													"clientId",
 763 | 													"redirectURLs",
 764 | 													"type",
 765 | 													"authenticationScheme",
 766 | 													"disabled",
 767 | 													"createdAt",
 768 | 													"updatedAt",
 769 | 												],
 770 | 											},
 771 | 										},
 772 | 									},
 773 | 								},
 774 | 							},
 775 | 						},
 776 | 					},
 777 | 				},
 778 | 				async (ctx) => {
 779 | 					const body = ctx.body;
 780 | 					const session = await getSessionFromCtx(ctx);
 781 | 					ctx.setHeader("Access-Control-Allow-Origin", "*");
 782 | 					ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
 783 | 					ctx.setHeader(
 784 | 						"Access-Control-Allow-Headers",
 785 | 						"Content-Type, Authorization",
 786 | 					);
 787 | 					ctx.setHeader("Access-Control-Max-Age", "86400");
 788 | 					ctx.headers?.set("Access-Control-Max-Age", "86400");
 789 | 					if (
 790 | 						(!body.grant_types ||
 791 | 							body.grant_types.includes("authorization_code") ||
 792 | 							body.grant_types.includes("implicit")) &&
 793 | 						(!body.redirect_uris || body.redirect_uris.length === 0)
 794 | 					) {
 795 | 						throw new APIError("BAD_REQUEST", {
 796 | 							error: "invalid_redirect_uri",
 797 | 							error_description:
 798 | 								"Redirect URIs are required for authorization_code and implicit grant types",
 799 | 						});
 800 | 					}
 801 | 
 802 | 					if (body.grant_types && body.response_types) {
 803 | 						if (
 804 | 							body.grant_types.includes("authorization_code") &&
 805 | 							!body.response_types.includes("code")
 806 | 						) {
 807 | 							throw new APIError("BAD_REQUEST", {
 808 | 								error: "invalid_client_metadata",
 809 | 								error_description:
 810 | 									"When 'authorization_code' grant type is used, 'code' response type must be included",
 811 | 							});
 812 | 						}
 813 | 						if (
 814 | 							body.grant_types.includes("implicit") &&
 815 | 							!body.response_types.includes("token")
 816 | 						) {
 817 | 							throw new APIError("BAD_REQUEST", {
 818 | 								error: "invalid_client_metadata",
 819 | 								error_description:
 820 | 									"When 'implicit' grant type is used, 'token' response type must be included",
 821 | 							});
 822 | 						}
 823 | 					}
 824 | 
 825 | 					const clientId =
 826 | 						opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
 827 | 					const clientSecret =
 828 | 						opts.generateClientSecret?.() ||
 829 | 						generateRandomString(32, "a-z", "A-Z");
 830 | 
 831 | 					// Determine client type based on auth method
 832 | 					const clientType =
 833 | 						body.token_endpoint_auth_method === "none" ? "public" : "web";
 834 | 					const finalClientSecret = clientType === "public" ? "" : clientSecret;
 835 | 
 836 | 					await ctx.context.adapter.create({
 837 | 						model: modelName.oauthClient,
 838 | 						data: {
 839 | 							name: body.client_name,
 840 | 							icon: body.logo_uri,
 841 | 							metadata: body.metadata ? JSON.stringify(body.metadata) : null,
 842 | 							clientId: clientId,
 843 | 							clientSecret: finalClientSecret,
 844 | 							redirectURLs: body.redirect_uris.join(","),
 845 | 							type: clientType,
 846 | 							authenticationScheme:
 847 | 								body.token_endpoint_auth_method || "client_secret_basic",
 848 | 							disabled: false,
 849 | 							userId: session?.session.userId,
 850 | 							createdAt: new Date(),
 851 | 							updatedAt: new Date(),
 852 | 						},
 853 | 					});
 854 | 
 855 | 					const responseData = {
 856 | 						client_id: clientId,
 857 | 						client_id_issued_at: Math.floor(Date.now() / 1000),
 858 | 						redirect_uris: body.redirect_uris,
 859 | 						token_endpoint_auth_method:
 860 | 							body.token_endpoint_auth_method || "client_secret_basic",
 861 | 						grant_types: body.grant_types || ["authorization_code"],
 862 | 						response_types: body.response_types || ["code"],
 863 | 						client_name: body.client_name,
 864 | 						client_uri: body.client_uri,
 865 | 						logo_uri: body.logo_uri,
 866 | 						scope: body.scope,
 867 | 						contacts: body.contacts,
 868 | 						tos_uri: body.tos_uri,
 869 | 						policy_uri: body.policy_uri,
 870 | 						jwks_uri: body.jwks_uri,
 871 | 						jwks: body.jwks,
 872 | 						software_id: body.software_id,
 873 | 						software_version: body.software_version,
 874 | 						software_statement: body.software_statement,
 875 | 						metadata: body.metadata,
 876 | 						...(clientType !== "public"
 877 | 							? {
 878 | 									client_secret: finalClientSecret,
 879 | 									client_secret_expires_at: 0, // 0 means it doesn't expire
 880 | 								}
 881 | 							: {}),
 882 | 					};
 883 | 
 884 | 					return new Response(JSON.stringify(responseData), {
 885 | 						status: 201,
 886 | 						headers: {
 887 | 							"Content-Type": "application/json",
 888 | 							"Cache-Control": "no-store",
 889 | 							Pragma: "no-cache",
 890 | 						},
 891 | 					});
 892 | 				},
 893 | 			),
 894 | 			getMcpSession: createAuthEndpoint(
 895 | 				"/mcp/get-session",
 896 | 				{
 897 | 					method: "GET",
 898 | 					requireHeaders: true,
 899 | 				},
 900 | 				async (c) => {
 901 | 					const accessToken = c.headers
 902 | 						?.get("Authorization")
 903 | 						?.replace("Bearer ", "");
 904 | 					if (!accessToken) {
 905 | 						c.headers?.set("WWW-Authenticate", "Bearer");
 906 | 						return c.json(null);
 907 | 					}
 908 | 					const accessTokenData =
 909 | 						await c.context.adapter.findOne<OAuthAccessToken>({
 910 | 							model: modelName.oauthAccessToken,
 911 | 							where: [
 912 | 								{
 913 | 									field: "accessToken",
 914 | 									value: accessToken,
 915 | 								},
 916 | 							],
 917 | 						});
 918 | 					if (!accessTokenData) {
 919 | 						return c.json(null);
 920 | 					}
 921 | 					return c.json(accessTokenData);
 922 | 				},
 923 | 			),
 924 | 		},
 925 | 		schema,
 926 | 	} satisfies BetterAuthPlugin;
 927 | };
 928 | 
 929 | export const withMcpAuth = <
 930 | 	Auth extends {
 931 | 		api: {
 932 | 			getMcpSession: (...args: any) => Promise<OAuthAccessToken | null>;
 933 | 		};
 934 | 		options: BetterAuthOptions;
 935 | 	},
 936 | >(
 937 | 	auth: Auth,
 938 | 	handler: (
 939 | 		req: Request,
 940 | 		sesssion: OAuthAccessToken,
 941 | 	) => Response | Promise<Response>,
 942 | ) => {
 943 | 	return async (req: Request) => {
 944 | 		const baseURL = getBaseURL(auth.options.baseURL, auth.options.basePath);
 945 | 		if (!baseURL && !isProduction) {
 946 | 			logger.warn("Unable to get the baseURL, please check your config!");
 947 | 		}
 948 | 		const session = await auth.api.getMcpSession({
 949 | 			headers: req.headers,
 950 | 		});
 951 | 		const wwwAuthenticateValue = `Bearer resource_metadata="${baseURL}/.well-known/oauth-protected-resource"`;
 952 | 		if (!session) {
 953 | 			return Response.json(
 954 | 				{
 955 | 					jsonrpc: "2.0",
 956 | 					error: {
 957 | 						code: -32000,
 958 | 						message: "Unauthorized: Authentication required",
 959 | 						"www-authenticate": wwwAuthenticateValue,
 960 | 					},
 961 | 					id: null,
 962 | 				},
 963 | 				{
 964 | 					status: 401,
 965 | 					headers: {
 966 | 						"WWW-Authenticate": wwwAuthenticateValue,
 967 | 						// we also add this headers otherwise browser based clients will not be able to read the `www-authenticate` header
 968 | 						"Access-Control-Expose-Headers": "WWW-Authenticate",
 969 | 					},
 970 | 				},
 971 | 			);
 972 | 		}
 973 | 		return handler(req, session);
 974 | 	};
 975 | };
 976 | 
 977 | export const oAuthDiscoveryMetadata = <
 978 | 	Auth extends {
 979 | 		api: {
 980 | 			getMcpOAuthConfig: (...args: any) => any;
 981 | 		};
 982 | 	},
 983 | >(
 984 | 	auth: Auth,
 985 | ) => {
 986 | 	return async (request: Request) => {
 987 | 		const res = await auth.api.getMcpOAuthConfig();
 988 | 		return new Response(JSON.stringify(res), {
 989 | 			status: 200,
 990 | 			headers: {
 991 | 				"Content-Type": "application/json",
 992 | 				"Access-Control-Allow-Origin": "*",
 993 | 				"Access-Control-Allow-Methods": "POST, OPTIONS",
 994 | 				"Access-Control-Allow-Headers": "Content-Type, Authorization",
 995 | 				"Access-Control-Max-Age": "86400",
 996 | 			},
 997 | 		});
 998 | 	};
 999 | };
1000 | 
1001 | export const oAuthProtectedResourceMetadata = <
1002 | 	Auth extends {
1003 | 		api: {
1004 | 			getMCPProtectedResource: (...args: any) => any;
1005 | 		};
1006 | 	},
1007 | >(
1008 | 	auth: Auth,
1009 | ) => {
1010 | 	return async (request: Request) => {
1011 | 		const res = await auth.api.getMCPProtectedResource();
1012 | 		return new Response(JSON.stringify(res), {
1013 | 			status: 200,
1014 | 			headers: {
1015 | 				"Content-Type": "application/json",
1016 | 				"Access-Control-Allow-Origin": "*",
1017 | 				"Access-Control-Allow-Methods": "POST, OPTIONS",
1018 | 				"Access-Control-Allow-Headers": "Content-Type, Authorization",
1019 | 				"Access-Control-Max-Age": "86400",
1020 | 			},
1021 | 		});
1022 | 	};
1023 | };
1024 | 
```
Page 52/67FirstPrevNextLast