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

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── e2e.yml
│       ├── preview.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│   └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│   ├── expo-example
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app.config.ts
│   │   ├── assets
│   │   │   ├── bg-image.jpeg
│   │   │   ├── fonts
│   │   │   │   └── SpaceMono-Regular.ttf
│   │   │   ├── icon.png
│   │   │   └── images
│   │   │       ├── adaptive-icon.png
│   │   │       ├── favicon.png
│   │   │       ├── logo.png
│   │   │       ├── partial-react-logo.png
│   │   │       ├── react-logo.png
│   │   │       ├── [email protected]
│   │   │       ├── [email protected]
│   │   │       └── splash.png
│   │   ├── babel.config.js
│   │   ├── components.json
│   │   ├── expo-env.d.ts
│   │   ├── index.ts
│   │   ├── metro.config.js
│   │   ├── nativewind-env.d.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── api
│   │   │   │   │   └── auth
│   │   │   │   │       └── [...route]+api.ts
│   │   │   │   ├── dashboard.tsx
│   │   │   │   ├── forget-password.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── sign-up.tsx
│   │   │   ├── components
│   │   │   │   ├── icons
│   │   │   │   │   └── google.tsx
│   │   │   │   └── ui
│   │   │   │       ├── avatar.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       └── text.tsx
│   │   │   ├── global.css
│   │   │   └── lib
│   │   │       ├── auth-client.ts
│   │   │       ├── auth.ts
│   │   │       ├── icons
│   │   │       │   ├── iconWithClassName.ts
│   │   │       │   └── X.tsx
│   │   │       └── utils.ts
│   │   ├── tailwind.config.js
│   │   └── tsconfig.json
│   └── nextjs
│       ├── .env.example
│       ├── .gitignore
│       ├── app
│       │   ├── (auth)
│       │   │   ├── forget-password
│       │   │   │   └── page.tsx
│       │   │   ├── reset-password
│       │   │   │   └── page.tsx
│       │   │   ├── sign-in
│       │   │   │   ├── loading.tsx
│       │   │   │   └── page.tsx
│       │   │   └── two-factor
│       │   │       ├── otp
│       │   │       │   └── page.tsx
│       │   │       └── page.tsx
│       │   ├── accept-invitation
│       │   │   └── [id]
│       │   │       ├── invitation-error.tsx
│       │   │       └── page.tsx
│       │   ├── admin
│       │   │   └── page.tsx
│       │   ├── api
│       │   │   └── auth
│       │   │       └── [...all]
│       │   │           └── route.ts
│       │   ├── apps
│       │   │   └── register
│       │   │       └── page.tsx
│       │   ├── client-test
│       │   │   └── page.tsx
│       │   ├── dashboard
│       │   │   ├── change-plan.tsx
│       │   │   ├── client.tsx
│       │   │   ├── organization-card.tsx
│       │   │   ├── page.tsx
│       │   │   ├── upgrade-button.tsx
│       │   │   └── user-card.tsx
│       │   ├── device
│       │   │   ├── approve
│       │   │   │   └── page.tsx
│       │   │   ├── denied
│       │   │   │   └── page.tsx
│       │   │   ├── layout.tsx
│       │   │   ├── page.tsx
│       │   │   └── success
│       │   │       └── page.tsx
│       │   ├── favicon.ico
│       │   ├── features.tsx
│       │   ├── fonts
│       │   │   ├── GeistMonoVF.woff
│       │   │   └── GeistVF.woff
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── oauth
│       │   │   └── authorize
│       │   │       ├── concet-buttons.tsx
│       │   │       └── page.tsx
│       │   ├── page.tsx
│       │   └── pricing
│       │       └── page.tsx
│       ├── components
│       │   ├── account-switch.tsx
│       │   ├── blocks
│       │   │   └── pricing.tsx
│       │   ├── logo.tsx
│       │   ├── one-tap.tsx
│       │   ├── sign-in-btn.tsx
│       │   ├── sign-in.tsx
│       │   ├── sign-up.tsx
│       │   ├── theme-provider.tsx
│       │   ├── theme-toggle.tsx
│       │   ├── tier-labels.tsx
│       │   ├── ui
│       │   │   ├── accordion.tsx
│       │   │   ├── alert-dialog.tsx
│       │   │   ├── alert.tsx
│       │   │   ├── aspect-ratio.tsx
│       │   │   ├── avatar.tsx
│       │   │   ├── badge.tsx
│       │   │   ├── breadcrumb.tsx
│       │   │   ├── button.tsx
│       │   │   ├── calendar.tsx
│       │   │   ├── card.tsx
│       │   │   ├── carousel.tsx
│       │   │   ├── chart.tsx
│       │   │   ├── checkbox.tsx
│       │   │   ├── collapsible.tsx
│       │   │   ├── command.tsx
│       │   │   ├── context-menu.tsx
│       │   │   ├── copy-button.tsx
│       │   │   ├── dialog.tsx
│       │   │   ├── drawer.tsx
│       │   │   ├── dropdown-menu.tsx
│       │   │   ├── form.tsx
│       │   │   ├── hover-card.tsx
│       │   │   ├── input-otp.tsx
│       │   │   ├── input.tsx
│       │   │   ├── label.tsx
│       │   │   ├── menubar.tsx
│       │   │   ├── navigation-menu.tsx
│       │   │   ├── pagination.tsx
│       │   │   ├── password-input.tsx
│       │   │   ├── popover.tsx
│       │   │   ├── progress.tsx
│       │   │   ├── radio-group.tsx
│       │   │   ├── resizable.tsx
│       │   │   ├── scroll-area.tsx
│       │   │   ├── select.tsx
│       │   │   ├── separator.tsx
│       │   │   ├── sheet.tsx
│       │   │   ├── skeleton.tsx
│       │   │   ├── slider.tsx
│       │   │   ├── sonner.tsx
│       │   │   ├── switch.tsx
│       │   │   ├── table.tsx
│       │   │   ├── tabs.tsx
│       │   │   ├── tabs2.tsx
│       │   │   ├── textarea.tsx
│       │   │   ├── toast.tsx
│       │   │   ├── toaster.tsx
│       │   │   ├── toggle-group.tsx
│       │   │   ├── toggle.tsx
│       │   │   └── tooltip.tsx
│       │   └── wrapper.tsx
│       ├── components.json
│       ├── hooks
│       │   └── use-toast.ts
│       ├── lib
│       │   ├── auth-client.ts
│       │   ├── auth-types.ts
│       │   ├── auth.ts
│       │   ├── email
│       │   │   ├── invitation.tsx
│       │   │   ├── resend.ts
│       │   │   └── reset-password.tsx
│       │   ├── metadata.ts
│       │   ├── shared.ts
│       │   └── utils.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── proxy.ts
│       ├── public
│       │   ├── __og.png
│       │   ├── _og.png
│       │   ├── favicon
│       │   │   ├── android-chrome-192x192.png
│       │   │   ├── android-chrome-512x512.png
│       │   │   ├── apple-touch-icon.png
│       │   │   ├── favicon-16x16.png
│       │   │   ├── favicon-32x32.png
│       │   │   ├── favicon.ico
│       │   │   ├── light
│       │   │   │   ├── android-chrome-192x192.png
│       │   │   │   ├── android-chrome-512x512.png
│       │   │   │   ├── apple-touch-icon.png
│       │   │   │   ├── favicon-16x16.png
│       │   │   │   ├── favicon-32x32.png
│       │   │   │   ├── favicon.ico
│       │   │   │   └── site.webmanifest
│       │   │   └── site.webmanifest
│       │   ├── logo.svg
│       │   └── og.png
│       ├── README.md
│       ├── tailwind.config.ts
│       ├── tsconfig.json
│       └── turbo.json
├── docker-compose.yml
├── docs
│   ├── .env.example
│   ├── .gitignore
│   ├── app
│   │   ├── api
│   │   │   ├── ai-chat
│   │   │   │   └── route.ts
│   │   │   ├── analytics
│   │   │   │   ├── conversation
│   │   │   │   │   └── route.ts
│   │   │   │   ├── event
│   │   │   │   │   └── route.ts
│   │   │   │   └── feedback
│   │   │   │       └── route.ts
│   │   │   ├── chat
│   │   │   │   └── route.ts
│   │   │   ├── og
│   │   │   │   └── route.tsx
│   │   │   ├── og-release
│   │   │   │   └── route.tsx
│   │   │   ├── search
│   │   │   │   └── route.ts
│   │   │   └── support
│   │   │       └── route.ts
│   │   ├── blog
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── blog-list.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   ├── stat-field.tsx
│   │   │   │   └── support.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── changelogs
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── grid-pattern.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   └── stat-field.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── community
│   │   │   ├── _components
│   │   │   │   ├── header.tsx
│   │   │   │   └── stats.tsx
│   │   │   └── page.tsx
│   │   ├── docs
│   │   │   ├── [[...slug]]
│   │   │   │   ├── page.client.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   └── lib
│   │   │       └── get-llm-text.ts
│   │   ├── global.css
│   │   ├── layout.config.tsx
│   │   ├── layout.tsx
│   │   ├── llms.txt
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   ├── reference
│   │   │   └── route.ts
│   │   ├── sitemap.xml
│   │   ├── static.json
│   │   │   └── route.ts
│   │   └── v1
│   │       ├── _components
│   │       │   └── v1-text.tsx
│   │       ├── bg-line.tsx
│   │       └── page.tsx
│   ├── assets
│   │   ├── Geist.ttf
│   │   └── GeistMono.ttf
│   ├── components
│   │   ├── ai-chat-modal.tsx
│   │   ├── anchor-scroll-fix.tsx
│   │   ├── api-method-tabs.tsx
│   │   ├── api-method.tsx
│   │   ├── banner.tsx
│   │   ├── blocks
│   │   │   └── features.tsx
│   │   ├── builder
│   │   │   ├── beam.tsx
│   │   │   ├── code-tabs
│   │   │   │   ├── code-editor.tsx
│   │   │   │   ├── code-tabs.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── tab-bar.tsx
│   │   │   │   └── theme.ts
│   │   │   ├── index.tsx
│   │   │   ├── sign-in.tsx
│   │   │   ├── sign-up.tsx
│   │   │   ├── social-provider.tsx
│   │   │   ├── store.ts
│   │   │   └── tabs.tsx
│   │   ├── display-techstack.tsx
│   │   ├── divider-text.tsx
│   │   ├── docs
│   │   │   ├── docs.client.tsx
│   │   │   ├── docs.tsx
│   │   │   ├── layout
│   │   │   │   ├── nav.tsx
│   │   │   │   ├── theme-toggle.tsx
│   │   │   │   ├── toc-thumb.tsx
│   │   │   │   └── toc.tsx
│   │   │   ├── page.client.tsx
│   │   │   ├── page.tsx
│   │   │   ├── shared.tsx
│   │   │   └── ui
│   │   │       ├── button.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── popover.tsx
│   │   │       └── scroll-area.tsx
│   │   ├── endpoint.tsx
│   │   ├── features.tsx
│   │   ├── floating-ai-search.tsx
│   │   ├── fork-button.tsx
│   │   ├── generate-apple-jwt.tsx
│   │   ├── generate-secret.tsx
│   │   ├── github-stat.tsx
│   │   ├── icons.tsx
│   │   ├── landing
│   │   │   ├── gradient-bg.tsx
│   │   │   ├── grid-pattern.tsx
│   │   │   ├── hero.tsx
│   │   │   ├── section-svg.tsx
│   │   │   ├── section.tsx
│   │   │   ├── spotlight.tsx
│   │   │   └── testimonials.tsx
│   │   ├── logo-context-menu.tsx
│   │   ├── logo.tsx
│   │   ├── markdown-renderer.tsx
│   │   ├── markdown.tsx
│   │   ├── mdx
│   │   │   ├── add-to-cursor.tsx
│   │   │   └── database-tables.tsx
│   │   ├── message-feedback.tsx
│   │   ├── mobile-search-icon.tsx
│   │   ├── nav-bar.tsx
│   │   ├── nav-link.tsx
│   │   ├── nav-mobile.tsx
│   │   ├── promo-card.tsx
│   │   ├── resource-card.tsx
│   │   ├── resource-grid.tsx
│   │   ├── resource-section.tsx
│   │   ├── ripple.tsx
│   │   ├── search-dialog.tsx
│   │   ├── side-bar.tsx
│   │   ├── sidebar-content.tsx
│   │   ├── techstack-icons.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggler.tsx
│   │   └── ui
│   │       ├── accordion.tsx
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── aside-link.tsx
│   │       ├── aspect-ratio.tsx
│   │       ├── avatar.tsx
│   │       ├── background-beams.tsx
│   │       ├── background-boxes.tsx
│   │       ├── badge.tsx
│   │       ├── breadcrumb.tsx
│   │       ├── button.tsx
│   │       ├── calendar.tsx
│   │       ├── callout.tsx
│   │       ├── card.tsx
│   │       ├── carousel.tsx
│   │       ├── chart.tsx
│   │       ├── checkbox.tsx
│   │       ├── code-block.tsx
│   │       ├── collapsible.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── dynamic-code-block.tsx
│   │       ├── fade-in.tsx
│   │       ├── form.tsx
│   │       ├── hover-card.tsx
│   │       ├── input-otp.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── menubar.tsx
│   │       ├── navigation-menu.tsx
│   │       ├── pagination.tsx
│   │       ├── popover.tsx
│   │       ├── progress.tsx
│   │       ├── radio-group.tsx
│   │       ├── resizable.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── separator.tsx
│   │       ├── sheet.tsx
│   │       ├── sidebar.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── sonner.tsx
│   │       ├── sparkles.tsx
│   │       ├── switch.tsx
│   │       ├── table.tsx
│   │       ├── tabs.tsx
│   │       ├── textarea.tsx
│   │       ├── toggle-group.tsx
│   │       ├── toggle.tsx
│   │       ├── tooltip-docs.tsx
│   │       ├── tooltip.tsx
│   │       └── use-copy-button.tsx
│   ├── components.json
│   ├── content
│   │   ├── blogs
│   │   │   ├── 0-supabase-auth-to-planetscale-migration.mdx
│   │   │   ├── 1-3.mdx
│   │   │   ├── authjs-joins-better-auth.mdx
│   │   │   └── seed-round.mdx
│   │   ├── changelogs
│   │   │   ├── 1-2.mdx
│   │   │   └── 1.0.mdx
│   │   └── docs
│   │       ├── adapters
│   │       │   ├── community-adapters.mdx
│   │       │   ├── drizzle.mdx
│   │       │   ├── mongo.mdx
│   │       │   ├── mssql.mdx
│   │       │   ├── mysql.mdx
│   │       │   ├── other-relational-databases.mdx
│   │       │   ├── postgresql.mdx
│   │       │   ├── prisma.mdx
│   │       │   └── sqlite.mdx
│   │       ├── authentication
│   │       │   ├── apple.mdx
│   │       │   ├── atlassian.mdx
│   │       │   ├── cognito.mdx
│   │       │   ├── discord.mdx
│   │       │   ├── dropbox.mdx
│   │       │   ├── email-password.mdx
│   │       │   ├── facebook.mdx
│   │       │   ├── figma.mdx
│   │       │   ├── github.mdx
│   │       │   ├── gitlab.mdx
│   │       │   ├── google.mdx
│   │       │   ├── huggingface.mdx
│   │       │   ├── kakao.mdx
│   │       │   ├── kick.mdx
│   │       │   ├── line.mdx
│   │       │   ├── linear.mdx
│   │       │   ├── linkedin.mdx
│   │       │   ├── microsoft.mdx
│   │       │   ├── naver.mdx
│   │       │   ├── notion.mdx
│   │       │   ├── other-social-providers.mdx
│   │       │   ├── paypal.mdx
│   │       │   ├── reddit.mdx
│   │       │   ├── roblox.mdx
│   │       │   ├── salesforce.mdx
│   │       │   ├── slack.mdx
│   │       │   ├── spotify.mdx
│   │       │   ├── tiktok.mdx
│   │       │   ├── twitch.mdx
│   │       │   ├── twitter.mdx
│   │       │   ├── vk.mdx
│   │       │   └── zoom.mdx
│   │       ├── basic-usage.mdx
│   │       ├── comparison.mdx
│   │       ├── concepts
│   │       │   ├── api.mdx
│   │       │   ├── cli.mdx
│   │       │   ├── client.mdx
│   │       │   ├── cookies.mdx
│   │       │   ├── database.mdx
│   │       │   ├── email.mdx
│   │       │   ├── hooks.mdx
│   │       │   ├── oauth.mdx
│   │       │   ├── plugins.mdx
│   │       │   ├── rate-limit.mdx
│   │       │   ├── session-management.mdx
│   │       │   ├── typescript.mdx
│   │       │   └── users-accounts.mdx
│   │       ├── examples
│   │       │   ├── astro.mdx
│   │       │   ├── next-js.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   └── svelte-kit.mdx
│   │       ├── guides
│   │       │   ├── auth0-migration-guide.mdx
│   │       │   ├── browser-extension-guide.mdx
│   │       │   ├── clerk-migration-guide.mdx
│   │       │   ├── create-a-db-adapter.mdx
│   │       │   ├── next-auth-migration-guide.mdx
│   │       │   ├── optimizing-for-performance.mdx
│   │       │   ├── saml-sso-with-okta.mdx
│   │       │   ├── supabase-migration-guide.mdx
│   │       │   └── your-first-plugin.mdx
│   │       ├── installation.mdx
│   │       ├── integrations
│   │       │   ├── astro.mdx
│   │       │   ├── convex.mdx
│   │       │   ├── elysia.mdx
│   │       │   ├── expo.mdx
│   │       │   ├── express.mdx
│   │       │   ├── fastify.mdx
│   │       │   ├── hono.mdx
│   │       │   ├── lynx.mdx
│   │       │   ├── nestjs.mdx
│   │       │   ├── next.mdx
│   │       │   ├── nitro.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   ├── solid-start.mdx
│   │       │   ├── svelte-kit.mdx
│   │       │   ├── tanstack.mdx
│   │       │   └── waku.mdx
│   │       ├── introduction.mdx
│   │       ├── meta.json
│   │       ├── plugins
│   │       │   ├── 2fa.mdx
│   │       │   ├── admin.mdx
│   │       │   ├── anonymous.mdx
│   │       │   ├── api-key.mdx
│   │       │   ├── autumn.mdx
│   │       │   ├── bearer.mdx
│   │       │   ├── captcha.mdx
│   │       │   ├── community-plugins.mdx
│   │       │   ├── device-authorization.mdx
│   │       │   ├── dodopayments.mdx
│   │       │   ├── dub.mdx
│   │       │   ├── email-otp.mdx
│   │       │   ├── generic-oauth.mdx
│   │       │   ├── have-i-been-pwned.mdx
│   │       │   ├── jwt.mdx
│   │       │   ├── last-login-method.mdx
│   │       │   ├── magic-link.mdx
│   │       │   ├── mcp.mdx
│   │       │   ├── multi-session.mdx
│   │       │   ├── oauth-proxy.mdx
│   │       │   ├── oidc-provider.mdx
│   │       │   ├── one-tap.mdx
│   │       │   ├── one-time-token.mdx
│   │       │   ├── open-api.mdx
│   │       │   ├── organization.mdx
│   │       │   ├── passkey.mdx
│   │       │   ├── phone-number.mdx
│   │       │   ├── polar.mdx
│   │       │   ├── siwe.mdx
│   │       │   ├── sso.mdx
│   │       │   ├── stripe.mdx
│   │       │   └── username.mdx
│   │       └── reference
│   │           ├── contributing.mdx
│   │           ├── faq.mdx
│   │           ├── options.mdx
│   │           ├── resources.mdx
│   │           ├── security.mdx
│   │           └── telemetry.mdx
│   ├── hooks
│   │   └── use-mobile.ts
│   ├── ignore-build.sh
│   ├── lib
│   │   ├── blog.ts
│   │   ├── chat
│   │   │   └── inkeep-qa-schema.ts
│   │   ├── constants.ts
│   │   ├── export-search-indexes.ts
│   │   ├── inkeep-analytics.ts
│   │   ├── is-active.ts
│   │   ├── metadata.ts
│   │   ├── source.ts
│   │   └── utils.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── proxy.ts
│   ├── public
│   │   ├── avatars
│   │   │   └── beka.jpg
│   │   ├── blogs
│   │   │   ├── authjs-joins.png
│   │   │   ├── seed-round.png
│   │   │   └── supabase-ps.png
│   │   ├── branding
│   │   │   ├── better-auth-brand-assets.zip
│   │   │   ├── better-auth-logo-dark.png
│   │   │   ├── better-auth-logo-dark.svg
│   │   │   ├── better-auth-logo-light.png
│   │   │   ├── better-auth-logo-light.svg
│   │   │   ├── better-auth-logo-wordmark-dark.png
│   │   │   ├── better-auth-logo-wordmark-dark.svg
│   │   │   ├── better-auth-logo-wordmark-light.png
│   │   │   └── better-auth-logo-wordmark-light.svg
│   │   ├── extension-id.png
│   │   ├── favicon
│   │   │   ├── android-chrome-192x192.png
│   │   │   ├── android-chrome-512x512.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── favicon-16x16.png
│   │   │   ├── favicon-32x32.png
│   │   │   ├── favicon.ico
│   │   │   ├── light
│   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   ├── apple-touch-icon.png
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   └── site.webmanifest
│   │   │   └── site.webmanifest
│   │   ├── images
│   │   │   └── blogs
│   │   │       └── better auth (1).png
│   │   ├── logo.png
│   │   ├── logo.svg
│   │   ├── LogoDark.webp
│   │   ├── LogoLight.webp
│   │   ├── og.png
│   │   ├── open-api-reference.png
│   │   ├── people-say
│   │   │   ├── code-with-antonio.jpg
│   │   │   ├── dagmawi-babi.png
│   │   │   ├── dax.png
│   │   │   ├── dev-ed.png
│   │   │   ├── egoist.png
│   │   │   ├── guillermo-rauch.png
│   │   │   ├── jonathan-wilke.png
│   │   │   ├── josh-tried-coding.jpg
│   │   │   ├── kitze.jpg
│   │   │   ├── lazar-nikolov.png
│   │   │   ├── nizzy.png
│   │   │   ├── omar-mcadam.png
│   │   │   ├── ryan-vogel.jpg
│   │   │   ├── saltyatom.jpg
│   │   │   ├── sebastien-chopin.png
│   │   │   ├── shreyas-mididoddi.png
│   │   │   ├── tech-nerd.png
│   │   │   ├── theo.png
│   │   │   ├── vybhav-bhargav.png
│   │   │   └── xavier-pladevall.jpg
│   │   ├── plus.svg
│   │   ├── release-og
│   │   │   ├── 1-2.png
│   │   │   ├── 1-3.png
│   │   │   └── changelog-og.png
│   │   └── v1-og.png
│   ├── README.md
│   ├── scripts
│   │   ├── endpoint-to-doc
│   │   │   ├── index.ts
│   │   │   ├── input.ts
│   │   │   ├── output.mdx
│   │   │   └── readme.md
│   │   └── sync-orama.ts
│   ├── source.config.ts
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── turbo.json
├── e2e
│   ├── integration
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── solid-vinxi
│   │   │   ├── .gitignore
│   │   │   ├── app.config.ts
│   │   │   ├── e2e
│   │   │   │   ├── test.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── entry-client.tsx
│   │   │   │   ├── entry-server.tsx
│   │   │   │   ├── global.d.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── auth-client.ts
│   │   │   │   │   └── auth.ts
│   │   │   │   └── routes
│   │   │   │       ├── [...404].tsx
│   │   │   │       ├── api
│   │   │   │       │   └── auth
│   │   │   │       │       └── [...all].ts
│   │   │   │       └── index.tsx
│   │   │   └── tsconfig.json
│   │   ├── test-utils
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── playwright.ts
│   │   └── vanilla-node
│   │       ├── e2e
│   │       │   ├── app.ts
│   │       │   ├── domain.spec.ts
│   │       │   ├── postgres-js.spec.ts
│   │       │   ├── test.spec.ts
│   │       │   └── utils.ts
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── src
│   │       │   ├── main.ts
│   │       │   └── vite-env.d.ts
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   └── smoke
│       ├── package.json
│       ├── test
│       │   ├── bun.spec.ts
│       │   ├── cloudflare.spec.ts
│       │   ├── deno.spec.ts
│       │   ├── fixtures
│       │   │   ├── bun-simple.ts
│       │   │   ├── cloudflare
│       │   │   │   ├── .gitignore
│       │   │   │   ├── drizzle
│       │   │   │   │   ├── 0000_clean_vector.sql
│       │   │   │   │   └── meta
│       │   │   │   │       ├── _journal.json
│       │   │   │   │       └── 0000_snapshot.json
│       │   │   │   ├── drizzle.config.ts
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── auth-schema.ts
│       │   │   │   │   ├── db.ts
│       │   │   │   │   └── index.ts
│       │   │   │   ├── test
│       │   │   │   │   ├── apply-migrations.ts
│       │   │   │   │   ├── env.d.ts
│       │   │   │   │   └── index.test.ts
│       │   │   │   ├── tsconfig.json
│       │   │   │   ├── vitest.config.ts
│       │   │   │   ├── worker-configuration.d.ts
│       │   │   │   └── wrangler.json
│       │   │   ├── deno-simple.ts
│       │   │   ├── tsconfig-declaration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-exact-optional-property-types
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── index.ts
│       │   │   │   │   ├── organization.ts
│       │   │   │   │   ├── user-additional-fields.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-isolated-module-bundler
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-verbatim-module-syntax-node10
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   └── vite
│       │   │       ├── package.json
│       │   │       ├── src
│       │   │       │   ├── client.ts
│       │   │       │   └── server.ts
│       │   │       ├── tsconfig.json
│       │   │       └── vite.config.ts
│       │   ├── ssr.ts
│       │   ├── typecheck.spec.ts
│       │   └── vite.spec.ts
│       └── tsconfig.json
├── LICENSE.md
├── package.json
├── packages
│   ├── better-auth
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── __snapshots__
│   │   │   │   └── init.test.ts.snap
│   │   │   ├── adapters
│   │   │   │   ├── adapter-factory
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── __snapshots__
│   │   │   │   │   │   │   └── adapter-factory.test.ts.snap
│   │   │   │   │   │   └── adapter-factory.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── create-test-suite.ts
│   │   │   │   ├── drizzle-adapter
│   │   │   │   │   ├── drizzle-adapter.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── adapter.drizzle.mysql.test.ts
│   │   │   │   │       ├── adapter.drizzle.pg.test.ts
│   │   │   │   │       ├── adapter.drizzle.sqlite.test.ts
│   │   │   │   │       └── generate-schema.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely-adapter
│   │   │   │   │   ├── bun-sqlite-dialect.ts
│   │   │   │   │   ├── dialect.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── kysely-adapter.ts
│   │   │   │   │   ├── node-sqlite-dialect.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── adapter.kysely.mssql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.mysql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg.test.ts
│   │   │   │   │   │   ├── adapter.kysely.sqlite.test.ts
│   │   │   │   │   │   └── node-sqlite-dialect.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── memory-adapter
│   │   │   │   │   ├── adapter.memory.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── memory-adapter.ts
│   │   │   │   ├── mongodb-adapter
│   │   │   │   │   ├── adapter.mongo-db.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mongodb-adapter.ts
│   │   │   │   ├── prisma-adapter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prisma-adapter.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── base.prisma
│   │   │   │   │       ├── generate-auth-config.ts
│   │   │   │   │       ├── generate-prisma-schema.ts
│   │   │   │   │       ├── get-prisma-client.ts
│   │   │   │   │       ├── prisma.mysql.test.ts
│   │   │   │   │       ├── prisma.pg.test.ts
│   │   │   │   │       ├── prisma.sqlite.test.ts
│   │   │   │   │       └── push-prisma-schema.ts
│   │   │   │   ├── test-adapter.ts
│   │   │   │   ├── test.ts
│   │   │   │   ├── tests
│   │   │   │   │   ├── auth-flow.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── normal.ts
│   │   │   │   │   ├── number-id.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── transactions.ts
│   │   │   │   └── utils.ts
│   │   │   ├── api
│   │   │   │   ├── check-endpoint-conflicts.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── middlewares
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── origin-check.test.ts
│   │   │   │   │   └── origin-check.ts
│   │   │   │   ├── rate-limiter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── rate-limiter.test.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── account.test.ts
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── callback.ts
│   │   │   │   │   ├── email-verification.test.ts
│   │   │   │   │   ├── email-verification.ts
│   │   │   │   │   ├── error.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── ok.ts
│   │   │   │   │   ├── reset-password.test.ts
│   │   │   │   │   ├── reset-password.ts
│   │   │   │   │   ├── session-api.test.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── sign-in.test.ts
│   │   │   │   │   ├── sign-in.ts
│   │   │   │   │   ├── sign-out.test.ts
│   │   │   │   │   ├── sign-out.ts
│   │   │   │   │   ├── sign-up.test.ts
│   │   │   │   │   ├── sign-up.ts
│   │   │   │   │   ├── update-user.test.ts
│   │   │   │   │   └── update-user.ts
│   │   │   │   ├── to-auth-endpoints.test.ts
│   │   │   │   └── to-auth-endpoints.ts
│   │   │   ├── auth.test.ts
│   │   │   ├── auth.ts
│   │   │   ├── call.test.ts
│   │   │   ├── client
│   │   │   │   ├── client-ssr.test.ts
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── fetch-plugins.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lynx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lynx-store.ts
│   │   │   │   ├── parser.ts
│   │   │   │   ├── path-to-object.ts
│   │   │   │   ├── plugins
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── infer-plugin.ts
│   │   │   │   ├── proxy.ts
│   │   │   │   ├── query.ts
│   │   │   │   ├── react
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── react-store.ts
│   │   │   │   ├── session-atom.ts
│   │   │   │   ├── solid
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── solid-store.ts
│   │   │   │   ├── svelte
│   │   │   │   │   └── index.ts
│   │   │   │   ├── test-plugin.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── url.test.ts
│   │   │   │   ├── vanilla.ts
│   │   │   │   └── vue
│   │   │   │       ├── index.ts
│   │   │   │       └── vue-store.ts
│   │   │   ├── cookies
│   │   │   │   ├── check-cookies.ts
│   │   │   │   ├── cookie-utils.ts
│   │   │   │   ├── cookies.test.ts
│   │   │   │   └── index.ts
│   │   │   ├── crypto
│   │   │   │   ├── buffer.ts
│   │   │   │   ├── hash.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt.ts
│   │   │   │   ├── password.test.ts
│   │   │   │   ├── password.ts
│   │   │   │   └── random.ts
│   │   │   ├── db
│   │   │   │   ├── db.test.ts
│   │   │   │   ├── field.ts
│   │   │   │   ├── get-migration.ts
│   │   │   │   ├── get-schema.ts
│   │   │   │   ├── get-tables.test.ts
│   │   │   │   ├── get-tables.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── internal-adapter.test.ts
│   │   │   │   ├── internal-adapter.ts
│   │   │   │   ├── schema.ts
│   │   │   │   ├── secondary-storage.test.ts
│   │   │   │   ├── to-zod.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── with-hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── init.test.ts
│   │   │   ├── init.ts
│   │   │   ├── integrations
│   │   │   │   ├── next-js.ts
│   │   │   │   ├── node.ts
│   │   │   │   ├── react-start.ts
│   │   │   │   ├── solid-start.ts
│   │   │   │   └── svelte-kit.ts
│   │   │   ├── oauth2
│   │   │   │   ├── index.ts
│   │   │   │   ├── link-account.test.ts
│   │   │   │   ├── link-account.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── utils.ts
│   │   │   ├── plugins
│   │   │   │   ├── access
│   │   │   │   │   ├── access.test.ts
│   │   │   │   │   ├── access.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── additional-fields
│   │   │   │   │   ├── additional-fields.test.ts
│   │   │   │   │   └── client.ts
│   │   │   │   ├── admin
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── admin.test.ts
│   │   │   │   │   ├── admin.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── anonymous
│   │   │   │   │   ├── anon.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── api-key
│   │   │   │   │   ├── api-key.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── create-api-key.ts
│   │   │   │   │   │   ├── delete-all-expired-api-keys.ts
│   │   │   │   │   │   ├── delete-api-key.ts
│   │   │   │   │   │   ├── get-api-key.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── list-api-keys.ts
│   │   │   │   │   │   ├── update-api-key.ts
│   │   │   │   │   │   └── verify-api-key.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── bearer
│   │   │   │   │   ├── bearer.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── captcha
│   │   │   │   │   ├── captcha.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-handlers
│   │   │   │   │       ├── captchafox.ts
│   │   │   │   │       ├── cloudflare-turnstile.ts
│   │   │   │   │       ├── google-recaptcha.ts
│   │   │   │   │       ├── h-captcha.ts
│   │   │   │   │       └── index.ts
│   │   │   │   ├── custom-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-session.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── device-authorization
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── device-authorization.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── email-otp
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── email-otp.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── generic-oauth
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── generic-oauth.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── haveibeenpwned
│   │   │   │   │   ├── haveibeenpwned.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── jwt.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── sign.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── last-login-method
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-prefix.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── last-login-method.test.ts
│   │   │   │   ├── magic-link
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── magic-link.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── mcp
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mcp.test.ts
│   │   │   │   ├── multi-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── multi-session.test.ts
│   │   │   │   ├── oauth-proxy
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── oauth-proxy.test.ts
│   │   │   │   ├── oidc-provider
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── oidc.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── ui.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── one-tap
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── one-time-token
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── one-time-token.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── open-api
│   │   │   │   │   ├── generator.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── logo.ts
│   │   │   │   │   └── open-api.test.ts
│   │   │   │   ├── organization
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── call.ts
│   │   │   │   │   ├── client.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── organization-hook.test.ts
│   │   │   │   │   ├── organization.test.ts
│   │   │   │   │   ├── organization.ts
│   │   │   │   │   ├── permission.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── crud-access-control.test.ts
│   │   │   │   │   │   ├── crud-access-control.ts
│   │   │   │   │   │   ├── crud-invites.ts
│   │   │   │   │   │   ├── crud-members.test.ts
│   │   │   │   │   │   ├── crud-members.ts
│   │   │   │   │   │   ├── crud-org.test.ts
│   │   │   │   │   │   ├── crud-org.ts
│   │   │   │   │   │   └── crud-team.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── team.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── passkey
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── passkey.test.ts
│   │   │   │   ├── phone-number
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── phone-number-error.ts
│   │   │   │   │   └── phone-number.test.ts
│   │   │   │   ├── siwe
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── siwe.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── two-factor
│   │   │   │   │   ├── backup-codes
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── constant.ts
│   │   │   │   │   ├── error-code.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── otp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── totp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── two-factor.test.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-two-factor.ts
│   │   │   │   └── username
│   │   │   │       ├── client.ts
│   │   │   │       ├── error-codes.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       └── username.test.ts
│   │   │   ├── social-providers
│   │   │   │   └── index.ts
│   │   │   ├── social.test.ts
│   │   │   ├── test-utils
│   │   │   │   ├── headers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── test-instance.ts
│   │   │   ├── types
│   │   │   │   ├── adapter.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── plugins.ts
│   │   │   │   └── types.test.ts
│   │   │   └── utils
│   │   │       ├── await-object.ts
│   │   │       ├── boolean.ts
│   │   │       ├── clone.ts
│   │   │       ├── constants.ts
│   │   │       ├── date.ts
│   │   │       ├── ensure-utc.ts
│   │   │       ├── get-request-ip.ts
│   │   │       ├── hashing.ts
│   │   │       ├── hide-metadata.ts
│   │   │       ├── id.ts
│   │   │       ├── import-util.ts
│   │   │       ├── index.ts
│   │   │       ├── is-atom.ts
│   │   │       ├── is-promise.ts
│   │   │       ├── json.ts
│   │   │       ├── merger.ts
│   │   │       ├── middleware-response.ts
│   │   │       ├── misc.ts
│   │   │       ├── password.ts
│   │   │       ├── plugin-helper.ts
│   │   │       ├── shim.ts
│   │   │       ├── time.ts
│   │   │       ├── url.ts
│   │   │       └── wildcard.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   ├── vitest.config.ts
│   │   └── vitest.setup.ts
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── api
│   │   │   │   └── index.ts
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── endpoint-context.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── transaction.ts
│   │   │   ├── db
│   │   │   │   ├── adapter
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── plugin.ts
│   │   │   │   ├── schema
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── user.ts
│   │   │   │   │   └── verification.ts
│   │   │   │   └── type.ts
│   │   │   ├── env
│   │   │   │   ├── color-depth.ts
│   │   │   │   ├── env-impl.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.test.ts
│   │   │   │   └── logger.ts
│   │   │   ├── error
│   │   │   │   ├── codes.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.base.json
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/magic-link/magic-link.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, vi } from "vitest";
  2 | import { getTestInstance } from "../../test-utils/test-instance";
  3 | import { magicLink } from ".";
  4 | import { createAuthClient } from "../../client";
  5 | import { magicLinkClient } from "./client";
  6 | import { defaultKeyHasher } from "./utils";
  7 | 
  8 | type VerificationEmail = {
  9 | 	email: string;
 10 | 	token: string;
 11 | 	url: string;
 12 | };
 13 | 
 14 | describe("magic link", async () => {
 15 | 	let verificationEmail: VerificationEmail = {
 16 | 		email: "",
 17 | 		token: "",
 18 | 		url: "",
 19 | 	};
 20 | 	const { customFetchImpl, testUser, sessionSetter } = await getTestInstance({
 21 | 		plugins: [
 22 | 			magicLink({
 23 | 				async sendMagicLink(data) {
 24 | 					verificationEmail = data;
 25 | 				},
 26 | 			}),
 27 | 		],
 28 | 	});
 29 | 
 30 | 	const client = createAuthClient({
 31 | 		plugins: [magicLinkClient()],
 32 | 		fetchOptions: {
 33 | 			customFetchImpl,
 34 | 		},
 35 | 		baseURL: "http://localhost:3000",
 36 | 		basePath: "/api/auth",
 37 | 	});
 38 | 
 39 | 	it("should send magic link", async () => {
 40 | 		await client.signIn.magicLink({
 41 | 			email: testUser.email,
 42 | 		});
 43 | 		expect(verificationEmail).toMatchObject({
 44 | 			email: testUser.email,
 45 | 			url: expect.stringContaining(
 46 | 				"http://localhost:3000/api/auth/magic-link/verify",
 47 | 			),
 48 | 		});
 49 | 	});
 50 | 	it("should verify magic link", async () => {
 51 | 		const headers = new Headers();
 52 | 		const response = await client.magicLink.verify({
 53 | 			query: {
 54 | 				token: new URL(verificationEmail.url).searchParams.get("token") || "",
 55 | 			},
 56 | 			fetchOptions: {
 57 | 				onSuccess: sessionSetter(headers),
 58 | 			},
 59 | 		});
 60 | 		expect(response.data?.token).toBeDefined();
 61 | 		const betterAuthCookie = headers.get("set-cookie");
 62 | 		expect(betterAuthCookie).toBeDefined();
 63 | 	});
 64 | 
 65 | 	it("shouldn't verify magic link with the same token", async () => {
 66 | 		await client.magicLink.verify(
 67 | 			{
 68 | 				query: {
 69 | 					token: new URL(verificationEmail.url).searchParams.get("token") || "",
 70 | 				},
 71 | 			},
 72 | 			{
 73 | 				onError(context) {
 74 | 					expect(context.response.status).toBe(302);
 75 | 					const location = context.response.headers.get("location");
 76 | 					expect(location).toContain("?error=INVALID_TOKEN");
 77 | 				},
 78 | 			},
 79 | 		);
 80 | 	});
 81 | 
 82 | 	it("shouldn't verify magic link with an expired token", async () => {
 83 | 		await client.signIn.magicLink({
 84 | 			email: testUser.email,
 85 | 		});
 86 | 		const token = verificationEmail.token;
 87 | 		vi.useFakeTimers();
 88 | 		await vi.advanceTimersByTimeAsync(1000 * 60 * 5 + 1);
 89 | 		await client.magicLink.verify(
 90 | 			{
 91 | 				query: {
 92 | 					token,
 93 | 					callbackURL: "/callback",
 94 | 				},
 95 | 			},
 96 | 			{
 97 | 				onError(context) {
 98 | 					expect(context.response.status).toBe(302);
 99 | 					const location = context.response.headers.get("location");
100 | 					expect(location).toContain("?error=EXPIRED_TOKEN");
101 | 				},
102 | 			},
103 | 		);
104 | 	});
105 | 
106 | 	it("should sign up with magic link", async () => {
107 | 		const email = "[email protected]";
108 | 		await client.signIn.magicLink({
109 | 			email,
110 | 			name: "test",
111 | 		});
112 | 		expect(verificationEmail).toMatchObject({
113 | 			email,
114 | 			url: expect.stringContaining(
115 | 				"http://localhost:3000/api/auth/magic-link/verify",
116 | 			),
117 | 		});
118 | 		const headers = new Headers();
119 | 		const response = await client.magicLink.verify({
120 | 			query: {
121 | 				token: new URL(verificationEmail.url).searchParams.get("token") || "",
122 | 			},
123 | 			fetchOptions: {
124 | 				onSuccess: sessionSetter(headers),
125 | 			},
126 | 		});
127 | 		const session = await client.getSession({
128 | 			fetchOptions: {
129 | 				headers,
130 | 			},
131 | 		});
132 | 		expect(session.data?.user).toMatchObject({
133 | 			name: "test",
134 | 			email: "[email protected]",
135 | 			emailVerified: true,
136 | 		});
137 | 	});
138 | 
139 | 	it("should use custom generateToken function", async () => {
140 | 		const customGenerateToken = vi.fn(() => "custom_token");
141 | 
142 | 		const { customFetchImpl } = await getTestInstance({
143 | 			plugins: [
144 | 				magicLink({
145 | 					async sendMagicLink(data) {
146 | 						verificationEmail = data;
147 | 					},
148 | 					generateToken: customGenerateToken,
149 | 				}),
150 | 			],
151 | 		});
152 | 
153 | 		const customClient = createAuthClient({
154 | 			plugins: [magicLinkClient()],
155 | 			fetchOptions: {
156 | 				customFetchImpl,
157 | 			},
158 | 			baseURL: "http://localhost:3000/api/auth",
159 | 		});
160 | 
161 | 		await customClient.signIn.magicLink({
162 | 			email: testUser.email,
163 | 		});
164 | 
165 | 		expect(customGenerateToken).toHaveBeenCalled();
166 | 		expect(verificationEmail.token).toBe("custom_token");
167 | 	});
168 | });
169 | 
170 | describe("magic link verify", async () => {
171 | 	const verificationEmail: VerificationEmail[] = [
172 | 		{
173 | 			email: "",
174 | 			token: "",
175 | 			url: "",
176 | 		},
177 | 	];
178 | 	const { customFetchImpl, testUser, sessionSetter } = await getTestInstance({
179 | 		plugins: [
180 | 			magicLink({
181 | 				async sendMagicLink(data) {
182 | 					verificationEmail.push(data);
183 | 				},
184 | 			}),
185 | 		],
186 | 	});
187 | 
188 | 	const client = createAuthClient({
189 | 		plugins: [magicLinkClient()],
190 | 		fetchOptions: {
191 | 			customFetchImpl,
192 | 		},
193 | 		baseURL: "http://localhost:3000/api/auth",
194 | 	});
195 | 
196 | 	it("should verify last magic link", async () => {
197 | 		await client.signIn.magicLink({
198 | 			email: testUser.email,
199 | 		});
200 | 		await client.signIn.magicLink({
201 | 			email: testUser.email,
202 | 		});
203 | 		await client.signIn.magicLink({
204 | 			email: testUser.email,
205 | 		});
206 | 		const headers = new Headers();
207 | 		const lastEmail = verificationEmail.pop() as VerificationEmail;
208 | 		const response = await client.magicLink.verify({
209 | 			query: {
210 | 				token: new URL(lastEmail.url).searchParams.get("token") || "",
211 | 			},
212 | 			fetchOptions: {
213 | 				onSuccess: sessionSetter(headers),
214 | 			},
215 | 		});
216 | 		expect(response.data?.token).toBeDefined();
217 | 		const betterAuthCookie = headers.get("set-cookie");
218 | 		expect(betterAuthCookie).toBeDefined();
219 | 	});
220 | });
221 | 
222 | describe("magic link storeToken", async () => {
223 | 	it("should store token in hashed", async () => {
224 | 		let verificationEmail: VerificationEmail = {
225 | 			email: "",
226 | 			token: "",
227 | 			url: "",
228 | 		};
229 | 		const { auth, signInWithTestUser, client, testUser } =
230 | 			await getTestInstance({
231 | 				plugins: [
232 | 					magicLink({
233 | 						storeToken: "hashed",
234 | 						sendMagicLink(data, request) {
235 | 							verificationEmail = data;
236 | 						},
237 | 					}),
238 | 				],
239 | 			});
240 | 
241 | 		const internalAdapter = (await auth.$context).internalAdapter;
242 | 		const { headers } = await signInWithTestUser();
243 | 		const response = await auth.api.signInMagicLink({
244 | 			body: {
245 | 				email: testUser.email,
246 | 			},
247 | 			headers,
248 | 		});
249 | 		const hashedToken = await defaultKeyHasher(verificationEmail.token);
250 | 		const storedToken =
251 | 			await internalAdapter.findVerificationValue(hashedToken);
252 | 		expect(storedToken).toBeDefined();
253 | 		const response2 = await auth.api.signInMagicLink({
254 | 			body: {
255 | 				email: testUser.email,
256 | 			},
257 | 			headers,
258 | 		});
259 | 		expect(response2.status).toBe(true);
260 | 	});
261 | 
262 | 	it("should store token with custom hasher", async () => {
263 | 		let verificationEmail: VerificationEmail = {
264 | 			email: "",
265 | 			token: "",
266 | 			url: "",
267 | 		};
268 | 		const { auth, signInWithTestUser, client, testUser } =
269 | 			await getTestInstance({
270 | 				plugins: [
271 | 					magicLink({
272 | 						storeToken: {
273 | 							type: "custom-hasher",
274 | 							async hash(token) {
275 | 								return token + "hashed";
276 | 							},
277 | 						},
278 | 						sendMagicLink(data, request) {
279 | 							verificationEmail = data;
280 | 						},
281 | 					}),
282 | 				],
283 | 			});
284 | 
285 | 		const internalAdapter = (await auth.$context).internalAdapter;
286 | 		const { headers } = await signInWithTestUser();
287 | 		await auth.api.signInMagicLink({
288 | 			body: {
289 | 				email: testUser.email,
290 | 			},
291 | 			headers,
292 | 		});
293 | 		const hashedToken = `${verificationEmail.token}hashed`;
294 | 		const storedToken =
295 | 			await internalAdapter.findVerificationValue(hashedToken);
296 | 		expect(storedToken).toBeDefined();
297 | 		const response2 = await auth.api.signInMagicLink({
298 | 			body: {
299 | 				email: testUser.email,
300 | 			},
301 | 			headers,
302 | 		});
303 | 		expect(response2.status).toBe(true);
304 | 	});
305 | });
306 | 
```

--------------------------------------------------------------------------------
/packages/core/src/social-providers/microsoft-entra-id.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 | 	validateAuthorizationCode,
  3 | 	createAuthorizationURL,
  4 | 	refreshAccessToken,
  5 | } from "../oauth2";
  6 | import type { OAuthProvider, ProviderOptions } from "../oauth2";
  7 | import { betterFetch } from "@better-fetch/fetch";
  8 | import { logger } from "../env";
  9 | import { decodeJwt } from "jose";
 10 | import { base64 } from "@better-auth/utils/base64";
 11 | 
 12 | /**
 13 |  * @see [Microsoft Identity Platform - Optional claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference)
 14 |  */
 15 | export interface MicrosoftEntraIDProfile extends Record<string, any> {
 16 | 	/** Identifies the intended recipient of the token */
 17 | 	aud: string;
 18 | 	/** Identifies the issuer, or "authorization server" that constructs and returns the token */
 19 | 	iss: string;
 20 | 	/** Indicates when the authentication for the token occurred */
 21 | 	iat: Date;
 22 | 	/** Records the identity provider that authenticated the subject of the token */
 23 | 	idp: string;
 24 | 	/** Identifies the time before which the JWT can't be accepted for processing */
 25 | 	nbf: Date;
 26 | 	/** Identifies the expiration time on or after which the JWT can't be accepted for processing */
 27 | 	exp: Date;
 28 | 	/** Code hash included in ID tokens when issued with an OAuth 2.0 authorization code */
 29 | 	c_hash: string;
 30 | 	/** Access token hash included in ID tokens when issued with an OAuth 2.0 access token */
 31 | 	at_hash: string;
 32 | 	/** Internal claim used to record data for token reuse */
 33 | 	aio: string;
 34 | 	/** The primary username that represents the user */
 35 | 	preferred_username: string;
 36 | 	/** User's email address */
 37 | 	email: string;
 38 | 	/** Human-readable value that identifies the subject of the token */
 39 | 	name: string;
 40 | 	/** Matches the parameter included in the original authorize request */
 41 | 	nonce: string;
 42 | 	/** User's profile picture */
 43 | 	picture: string;
 44 | 	/** Immutable identifier for the user account */
 45 | 	oid: string;
 46 | 	/** Set of roles assigned to the user */
 47 | 	roles: string[];
 48 | 	/** Internal claim used to revalidate tokens */
 49 | 	rh: string;
 50 | 	/** Subject identifier - unique to application ID */
 51 | 	sub: string;
 52 | 	/** Tenant ID the user is signing in to */
 53 | 	tid: string;
 54 | 	/** Unique identifier for a session */
 55 | 	sid: string;
 56 | 	/** Token identifier claim */
 57 | 	uti: string;
 58 | 	/** Indicates if user is in at least one group */
 59 | 	hasgroups: boolean;
 60 | 	/** User account status in tenant (0 = member, 1 = guest) */
 61 | 	acct: 0 | 1;
 62 | 	/** Auth Context IDs */
 63 | 	acrs: string;
 64 | 	/** Time when the user last authenticated */
 65 | 	auth_time: Date;
 66 | 	/** User's country/region */
 67 | 	ctry: string;
 68 | 	/** IP address of requesting client when inside VNET */
 69 | 	fwd: string;
 70 | 	/** Group claims */
 71 | 	groups: string;
 72 | 	/** Login hint for SSO */
 73 | 	login_hint: string;
 74 | 	/** Resource tenant's country/region */
 75 | 	tenant_ctry: string;
 76 | 	/** Region of the resource tenant */
 77 | 	tenant_region_scope: string;
 78 | 	/** UserPrincipalName */
 79 | 	upn: string;
 80 | 	/** User's verified primary email addresses */
 81 | 	verified_primary_email: string[];
 82 | 	/** User's verified secondary email addresses */
 83 | 	verified_secondary_email: string[];
 84 | 	/** VNET specifier information */
 85 | 	vnet: string;
 86 | 	/** Client Capabilities */
 87 | 	xms_cc: string;
 88 | 	/** Whether user's email domain is verified */
 89 | 	xms_edov: boolean;
 90 | 	/** Preferred data location for Multi-Geo tenants */
 91 | 	xms_pdl: string;
 92 | 	/** User preferred language */
 93 | 	xms_pl: string;
 94 | 	/** Tenant preferred language */
 95 | 	xms_tpl: string;
 96 | 	/** Zero-touch Deployment ID */
 97 | 	ztdid: string;
 98 | 	/** IP Address */
 99 | 	ipaddr: string;
100 | 	/** On-premises Security Identifier */
101 | 	onprem_sid: string;
102 | 	/** Password Expiration Time */
103 | 	pwd_exp: number;
104 | 	/** Change Password URL */
105 | 	pwd_url: string;
106 | 	/** Inside Corporate Network flag */
107 | 	in_corp: string;
108 | 	/** User's family name/surname */
109 | 	family_name: string;
110 | 	/** User's given/first name */
111 | 	given_name: string;
112 | }
113 | 
114 | export interface MicrosoftOptions
115 | 	extends ProviderOptions<MicrosoftEntraIDProfile> {
116 | 	clientId: string;
117 | 	/**
118 | 	 * The tenant ID of the Microsoft account
119 | 	 * @default "common"
120 | 	 */
121 | 	tenantId?: string;
122 | 	/**
123 | 	 * The authentication authority URL. Use the default "https://login.microsoftonline.com" for standard Entra ID or "https://<tenant-id>.ciamlogin.com" for CIAM scenarios.
124 | 	 * @default "https://login.microsoftonline.com"
125 | 	 */
126 | 	authority?: string;
127 | 	/**
128 | 	 * The size of the profile photo
129 | 	 * @default 48
130 | 	 */
131 | 	profilePhotoSize?: 48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648;
132 | 	/**
133 | 	 * Disable profile photo
134 | 	 */
135 | 	disableProfilePhoto?: boolean;
136 | }
137 | 
138 | export const microsoft = (options: MicrosoftOptions) => {
139 | 	const tenant = options.tenantId || "common";
140 | 	const authority = options.authority || "https://login.microsoftonline.com";
141 | 	const authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`;
142 | 	const tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;
143 | 	return {
144 | 		id: "microsoft",
145 | 		name: "Microsoft EntraID",
146 | 		createAuthorizationURL(data) {
147 | 			const scopes = options.disableDefaultScope
148 | 				? []
149 | 				: ["openid", "profile", "email", "User.Read", "offline_access"];
150 | 			options.scope && scopes.push(...options.scope);
151 | 			data.scopes && scopes.push(...data.scopes);
152 | 			return createAuthorizationURL({
153 | 				id: "microsoft",
154 | 				options,
155 | 				authorizationEndpoint,
156 | 				state: data.state,
157 | 				codeVerifier: data.codeVerifier,
158 | 				scopes,
159 | 				redirectURI: data.redirectURI,
160 | 				prompt: options.prompt,
161 | 				loginHint: data.loginHint,
162 | 			});
163 | 		},
164 | 		validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
165 | 			return validateAuthorizationCode({
166 | 				code,
167 | 				codeVerifier,
168 | 				redirectURI,
169 | 				options,
170 | 				tokenEndpoint,
171 | 			});
172 | 		},
173 | 		async getUserInfo(token) {
174 | 			if (options.getUserInfo) {
175 | 				return options.getUserInfo(token);
176 | 			}
177 | 			if (!token.idToken) {
178 | 				return null;
179 | 			}
180 | 			const user = decodeJwt(token.idToken) as MicrosoftEntraIDProfile;
181 | 			const profilePhotoSize = options.profilePhotoSize || 48;
182 | 			await betterFetch<ArrayBuffer>(
183 | 				`https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,
184 | 				{
185 | 					headers: {
186 | 						Authorization: `Bearer ${token.accessToken}`,
187 | 					},
188 | 					async onResponse(context) {
189 | 						if (options.disableProfilePhoto || !context.response.ok) {
190 | 							return;
191 | 						}
192 | 						try {
193 | 							const response = context.response.clone();
194 | 							const pictureBuffer = await response.arrayBuffer();
195 | 							const pictureBase64 = base64.encode(pictureBuffer);
196 | 							user.picture = `data:image/jpeg;base64, ${pictureBase64}`;
197 | 						} catch (e) {
198 | 							logger.error(
199 | 								e && typeof e === "object" && "name" in e
200 | 									? (e.name as string)
201 | 									: "",
202 | 								e,
203 | 							);
204 | 						}
205 | 					},
206 | 				},
207 | 			);
208 | 			const userMap = await options.mapProfileToUser?.(user);
209 | 			return {
210 | 				user: {
211 | 					id: user.sub,
212 | 					name: user.name,
213 | 					email: user.email,
214 | 					image: user.picture,
215 | 					emailVerified: true,
216 | 					...userMap,
217 | 				},
218 | 				data: user,
219 | 			};
220 | 		},
221 | 		refreshAccessToken: options.refreshAccessToken
222 | 			? options.refreshAccessToken
223 | 			: async (refreshToken) => {
224 | 					const scopes = options.disableDefaultScope
225 | 						? []
226 | 						: ["openid", "profile", "email", "User.Read", "offline_access"];
227 | 					options.scope && scopes.push(...options.scope);
228 | 
229 | 					return refreshAccessToken({
230 | 						refreshToken,
231 | 						options: {
232 | 							clientId: options.clientId,
233 | 							clientSecret: options.clientSecret,
234 | 						},
235 | 						extraParams: {
236 | 							scope: scopes.join(" "), // Include the scopes in request to microsoft
237 | 						},
238 | 						tokenEndpoint,
239 | 					});
240 | 				},
241 | 		options,
242 | 	} satisfies OAuthProvider;
243 | };
244 | 
```

--------------------------------------------------------------------------------
/packages/telemetry/src/detectors/detect-auth-config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { TelemetryContext } from "../types";
  2 | import type { BetterAuthOptions } from "@better-auth/core";
  3 | 
  4 | export function getTelemetryAuthConfig(
  5 | 	options: BetterAuthOptions,
  6 | 	context?: TelemetryContext,
  7 | ) {
  8 | 	return {
  9 | 		database: context?.database,
 10 | 		adapter: context?.adapter,
 11 | 		emailVerification: {
 12 | 			sendVerificationEmail: !!options.emailVerification?.sendVerificationEmail,
 13 | 			sendOnSignUp: !!options.emailVerification?.sendOnSignUp,
 14 | 			sendOnSignIn: !!options.emailVerification?.sendOnSignIn,
 15 | 			autoSignInAfterVerification:
 16 | 				!!options.emailVerification?.autoSignInAfterVerification,
 17 | 			expiresIn: options.emailVerification?.expiresIn,
 18 | 			onEmailVerification: !!options.emailVerification?.onEmailVerification,
 19 | 			afterEmailVerification:
 20 | 				!!options.emailVerification?.afterEmailVerification,
 21 | 		},
 22 | 		emailAndPassword: {
 23 | 			enabled: !!options.emailAndPassword?.enabled,
 24 | 			disableSignUp: !!options.emailAndPassword?.disableSignUp,
 25 | 			requireEmailVerification:
 26 | 				!!options.emailAndPassword?.requireEmailVerification,
 27 | 			maxPasswordLength: options.emailAndPassword?.maxPasswordLength,
 28 | 			minPasswordLength: options.emailAndPassword?.minPasswordLength,
 29 | 			sendResetPassword: !!options.emailAndPassword?.sendResetPassword,
 30 | 			resetPasswordTokenExpiresIn:
 31 | 				options.emailAndPassword?.resetPasswordTokenExpiresIn,
 32 | 			onPasswordReset: !!options.emailAndPassword?.onPasswordReset,
 33 | 			password: {
 34 | 				hash: !!options.emailAndPassword?.password?.hash,
 35 | 				verify: !!options.emailAndPassword?.password?.verify,
 36 | 			},
 37 | 			autoSignIn: !!options.emailAndPassword?.autoSignIn,
 38 | 			revokeSessionsOnPasswordReset:
 39 | 				!!options.emailAndPassword?.revokeSessionsOnPasswordReset,
 40 | 		},
 41 | 		socialProviders: Object.keys(options.socialProviders || {}).map((p) => {
 42 | 			const provider =
 43 | 				options.socialProviders?.[p as keyof typeof options.socialProviders];
 44 | 			if (!provider) return {};
 45 | 			return {
 46 | 				id: p,
 47 | 				mapProfileToUser: !!provider.mapProfileToUser,
 48 | 				disableDefaultScope: !!provider.disableDefaultScope,
 49 | 				disableIdTokenSignIn: !!provider.disableIdTokenSignIn,
 50 | 				disableImplicitSignUp: provider.disableImplicitSignUp,
 51 | 				disableSignUp: provider.disableSignUp,
 52 | 				getUserInfo: !!provider.getUserInfo,
 53 | 				overrideUserInfoOnSignIn: !!provider.overrideUserInfoOnSignIn,
 54 | 				prompt: provider.prompt,
 55 | 				verifyIdToken: !!provider.verifyIdToken,
 56 | 				scope: provider.scope,
 57 | 				refreshAccessToken: !!provider.refreshAccessToken,
 58 | 			};
 59 | 		}),
 60 | 		plugins: options.plugins?.map((p) => p.id.toString()),
 61 | 		user: {
 62 | 			modelName: options.user?.modelName,
 63 | 			fields: options.user?.fields,
 64 | 			additionalFields: options.user?.additionalFields,
 65 | 			changeEmail: {
 66 | 				enabled: options.user?.changeEmail?.enabled,
 67 | 				sendChangeEmailVerification:
 68 | 					!!options.user?.changeEmail?.sendChangeEmailVerification,
 69 | 			},
 70 | 		},
 71 | 		verification: {
 72 | 			modelName: options.verification?.modelName,
 73 | 			disableCleanup: options.verification?.disableCleanup,
 74 | 			fields: options.verification?.fields,
 75 | 		},
 76 | 		session: {
 77 | 			modelName: options.session?.modelName,
 78 | 			additionalFields: options.session?.additionalFields,
 79 | 			cookieCache: {
 80 | 				enabled: options.session?.cookieCache?.enabled,
 81 | 				maxAge: options.session?.cookieCache?.maxAge,
 82 | 			},
 83 | 			disableSessionRefresh: options.session?.disableSessionRefresh,
 84 | 			expiresIn: options.session?.expiresIn,
 85 | 			fields: options.session?.fields,
 86 | 			freshAge: options.session?.freshAge,
 87 | 			preserveSessionInDatabase: options.session?.preserveSessionInDatabase,
 88 | 			storeSessionInDatabase: options.session?.storeSessionInDatabase,
 89 | 			updateAge: options.session?.updateAge,
 90 | 		},
 91 | 		account: {
 92 | 			modelName: options.account?.modelName,
 93 | 			fields: options.account?.fields,
 94 | 			encryptOAuthTokens: options.account?.encryptOAuthTokens,
 95 | 			updateAccountOnSignIn: options.account?.updateAccountOnSignIn,
 96 | 			accountLinking: {
 97 | 				enabled: options.account?.accountLinking?.enabled,
 98 | 				trustedProviders: options.account?.accountLinking?.trustedProviders,
 99 | 				updateUserInfoOnLink:
100 | 					options.account?.accountLinking?.updateUserInfoOnLink,
101 | 				allowUnlinkingAll: options.account?.accountLinking?.allowUnlinkingAll,
102 | 			},
103 | 		},
104 | 		hooks: {
105 | 			after: !!options.hooks?.after,
106 | 			before: !!options.hooks?.before,
107 | 		},
108 | 		secondaryStorage: !!options.secondaryStorage,
109 | 		advanced: {
110 | 			cookiePrefix: !!options.advanced?.cookiePrefix, //this shouldn't be tracked
111 | 			cookies: !!options.advanced?.cookies,
112 | 			crossSubDomainCookies: {
113 | 				domain: !!options.advanced?.crossSubDomainCookies?.domain,
114 | 				enabled: options.advanced?.crossSubDomainCookies?.enabled,
115 | 				additionalCookies:
116 | 					options.advanced?.crossSubDomainCookies?.additionalCookies,
117 | 			},
118 | 			database: {
119 | 				useNumberId: !!options.advanced?.database?.useNumberId,
120 | 				generateId: options.advanced?.database?.generateId,
121 | 				defaultFindManyLimit: options.advanced?.database?.defaultFindManyLimit,
122 | 			},
123 | 			useSecureCookies: options.advanced?.useSecureCookies,
124 | 			ipAddress: {
125 | 				disableIpTracking: options.advanced?.ipAddress?.disableIpTracking,
126 | 				ipAddressHeaders: options.advanced?.ipAddress?.ipAddressHeaders,
127 | 			},
128 | 			disableCSRFCheck: options.advanced?.disableCSRFCheck,
129 | 			cookieAttributes: {
130 | 				expires: options.advanced?.defaultCookieAttributes?.expires,
131 | 				secure: options.advanced?.defaultCookieAttributes?.secure,
132 | 				sameSite: options.advanced?.defaultCookieAttributes?.sameSite,
133 | 				domain: !!options.advanced?.defaultCookieAttributes?.domain,
134 | 				path: options.advanced?.defaultCookieAttributes?.path,
135 | 				httpOnly: options.advanced?.defaultCookieAttributes?.httpOnly,
136 | 			},
137 | 		},
138 | 		trustedOrigins: options.trustedOrigins?.length,
139 | 		rateLimit: {
140 | 			storage: options.rateLimit?.storage,
141 | 			modelName: options.rateLimit?.modelName,
142 | 			window: options.rateLimit?.window,
143 | 			customStorage: !!options.rateLimit?.customStorage,
144 | 			enabled: options.rateLimit?.enabled,
145 | 			max: options.rateLimit?.max,
146 | 		},
147 | 		onAPIError: {
148 | 			errorURL: options.onAPIError?.errorURL,
149 | 			onError: !!options.onAPIError?.onError,
150 | 			throw: options.onAPIError?.throw,
151 | 		},
152 | 		logger: {
153 | 			disabled: options.logger?.disabled,
154 | 			level: options.logger?.level,
155 | 			log: !!options.logger?.log,
156 | 		},
157 | 		databaseHooks: {
158 | 			user: {
159 | 				create: {
160 | 					after: !!options.databaseHooks?.user?.create?.after,
161 | 					before: !!options.databaseHooks?.user?.create?.before,
162 | 				},
163 | 				update: {
164 | 					after: !!options.databaseHooks?.user?.update?.after,
165 | 					before: !!options.databaseHooks?.user?.update?.before,
166 | 				},
167 | 			},
168 | 			session: {
169 | 				create: {
170 | 					after: !!options.databaseHooks?.session?.create?.after,
171 | 					before: !!options.databaseHooks?.session?.create?.before,
172 | 				},
173 | 				update: {
174 | 					after: !!options.databaseHooks?.session?.update?.after,
175 | 					before: !!options.databaseHooks?.session?.update?.before,
176 | 				},
177 | 			},
178 | 			account: {
179 | 				create: {
180 | 					after: !!options.databaseHooks?.account?.create?.after,
181 | 					before: !!options.databaseHooks?.account?.create?.before,
182 | 				},
183 | 				update: {
184 | 					after: !!options.databaseHooks?.account?.update?.after,
185 | 					before: !!options.databaseHooks?.account?.update?.before,
186 | 				},
187 | 			},
188 | 			verification: {
189 | 				create: {
190 | 					after: !!options.databaseHooks?.verification?.create?.after,
191 | 					before: !!options.databaseHooks?.verification?.create?.before,
192 | 				},
193 | 				update: {
194 | 					after: !!options.databaseHooks?.verification?.update?.after,
195 | 					before: !!options.databaseHooks?.verification?.update?.before,
196 | 				},
197 | 			},
198 | 		},
199 | 	};
200 | }
201 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/components/sign-in.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import { Button } from "@/components/ui/button";
  4 | import {
  5 | 	Card,
  6 | 	CardContent,
  7 | 	CardHeader,
  8 | 	CardTitle,
  9 | 	CardDescription,
 10 | 	CardFooter,
 11 | } from "@/components/ui/card";
 12 | import { Input } from "@/components/ui/input";
 13 | import { Label } from "@/components/ui/label";
 14 | import { Checkbox } from "@/components/ui/checkbox";
 15 | import { useState, useTransition } from "react";
 16 | import { Loader2 } from "lucide-react";
 17 | import { client, signIn } from "@/lib/auth-client";
 18 | import Link from "next/link";
 19 | import { cn } from "@/lib/utils";
 20 | import { useRouter, useSearchParams } from "next/navigation";
 21 | import { toast } from "sonner";
 22 | import { getCallbackURL } from "@/lib/shared";
 23 | 
 24 | export default function SignIn() {
 25 | 	const [email, setEmail] = useState("");
 26 | 	const [password, setPassword] = useState("");
 27 | 	const [loading, startTransition] = useTransition();
 28 | 	const [rememberMe, setRememberMe] = useState(false);
 29 | 	const router = useRouter();
 30 | 	const params = useSearchParams();
 31 | 
 32 | 	const LastUsedIndicator = () => (
 33 | 		<span className="ml-auto absolute top-0 right-0 px-2 py-1 text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-md font-medium">
 34 | 			Last Used
 35 | 		</span>
 36 | 	);
 37 | 
 38 | 	return (
 39 | 		<Card className="max-w-md rounded-none">
 40 | 			<CardHeader>
 41 | 				<CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
 42 | 				<CardDescription className="text-xs md:text-sm">
 43 | 					Enter your email below to login to your account
 44 | 				</CardDescription>
 45 | 			</CardHeader>
 46 | 			<CardContent>
 47 | 				<div className="grid gap-4">
 48 | 					<div className="grid gap-2">
 49 | 						<Label htmlFor="email">Email</Label>
 50 | 						<Input
 51 | 							id="email"
 52 | 							type="email"
 53 | 							placeholder="[email protected]"
 54 | 							required
 55 | 							onChange={(e) => {
 56 | 								setEmail(e.target.value);
 57 | 							}}
 58 | 							value={email}
 59 | 						/>
 60 | 					</div>
 61 | 
 62 | 					<div className="grid gap-2">
 63 | 						<div className="flex items-center">
 64 | 							<Label htmlFor="password">Password</Label>
 65 | 							<Link
 66 | 								href="/forget-password"
 67 | 								className="ml-auto inline-block text-sm underline"
 68 | 							>
 69 | 								Forgot your password?
 70 | 							</Link>
 71 | 						</div>
 72 | 
 73 | 						<Input
 74 | 							id="password"
 75 | 							type="password"
 76 | 							placeholder="password"
 77 | 							autoComplete="password"
 78 | 							value={password}
 79 | 							onChange={(e) => setPassword(e.target.value)}
 80 | 						/>
 81 | 					</div>
 82 | 
 83 | 					<div className="flex items-center gap-2">
 84 | 						<Checkbox
 85 | 							id="remember"
 86 | 							onClick={() => {
 87 | 								setRememberMe(!rememberMe);
 88 | 							}}
 89 | 						/>
 90 | 						<Label htmlFor="remember">Remember me</Label>
 91 | 					</div>
 92 | 
 93 | 					<Button
 94 | 						type="submit"
 95 | 						className="w-full flex items-center justify-center"
 96 | 						disabled={loading}
 97 | 						onClick={async () => {
 98 | 							startTransition(async () => {
 99 | 								await signIn.email(
100 | 									{ email, password, rememberMe },
101 | 									{
102 | 										onSuccess(context) {
103 | 											toast.success("Successfully signed in");
104 | 											router.push(getCallbackURL(params));
105 | 										},
106 | 										onError(context) {
107 | 											toast.error(context.error.message);
108 | 										},
109 | 									},
110 | 								);
111 | 							});
112 | 						}}
113 | 					>
114 | 						<div className="flex items-center justify-center w-full relative">
115 | 							{loading ? (
116 | 								<Loader2 size={16} className="animate-spin" />
117 | 							) : (
118 | 								"Login"
119 | 							)}
120 | 							{client.isLastUsedLoginMethod("email") && <LastUsedIndicator />}
121 | 						</div>
122 | 					</Button>
123 | 
124 | 					<div
125 | 						className={cn(
126 | 							"w-full gap-2 flex items-center",
127 | 							"justify-between flex-col",
128 | 						)}
129 | 					>
130 | 						<Button
131 | 							variant="outline"
132 | 							className={cn("w-full gap-2 flex relative")}
133 | 							onClick={async () => {
134 | 								await signIn.social({
135 | 									provider: "google",
136 | 									callbackURL: "/dashboard",
137 | 								});
138 | 							}}
139 | 						>
140 | 							<svg
141 | 								xmlns="http://www.w3.org/2000/svg"
142 | 								width="0.98em"
143 | 								height="1em"
144 | 								viewBox="0 0 256 262"
145 | 							>
146 | 								<path
147 | 									fill="#4285F4"
148 | 									d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
149 | 								></path>
150 | 								<path
151 | 									fill="#34A853"
152 | 									d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
153 | 								></path>
154 | 								<path
155 | 									fill="#FBBC05"
156 | 									d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
157 | 								></path>
158 | 								<path
159 | 									fill="#EB4335"
160 | 									d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
161 | 								></path>
162 | 							</svg>
163 | 							<span>Sign in with Google</span>
164 | 							{client.isLastUsedLoginMethod("google") && <LastUsedIndicator />}
165 | 						</Button>
166 | 						<Button
167 | 							variant="outline"
168 | 							className={cn("w-full gap-2 flex items-center relative")}
169 | 							onClick={async () => {
170 | 								await signIn.social({
171 | 									provider: "github",
172 | 									callbackURL: "/dashboard",
173 | 								});
174 | 							}}
175 | 						>
176 | 							<svg
177 | 								xmlns="http://www.w3.org/2000/svg"
178 | 								width="1em"
179 | 								height="1em"
180 | 								viewBox="0 0 24 24"
181 | 							>
182 | 								<path
183 | 									fill="currentColor"
184 | 									d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
185 | 								></path>
186 | 							</svg>
187 | 							<span>Sign in with GitHub</span>
188 | 							{client.isLastUsedLoginMethod("github") && <LastUsedIndicator />}
189 | 						</Button>
190 | 						<Button
191 | 							variant="outline"
192 | 							className={cn("w-full gap-2 flex items-center relative")}
193 | 							onClick={async () => {
194 | 								await signIn.social({
195 | 									provider: "microsoft",
196 | 									callbackURL: "/dashboard",
197 | 								});
198 | 							}}
199 | 						>
200 | 							<svg
201 | 								xmlns="http://www.w3.org/2000/svg"
202 | 								width="1em"
203 | 								height="1em"
204 | 								viewBox="0 0 24 24"
205 | 							>
206 | 								<path
207 | 									fill="currentColor"
208 | 									d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
209 | 								></path>
210 | 							</svg>
211 | 							<span>Sign in with Microsoft</span>
212 | 							{client.isLastUsedLoginMethod("microsoft") && (
213 | 								<LastUsedIndicator />
214 | 							)}
215 | 						</Button>
216 | 					</div>
217 | 				</div>
218 | 			</CardContent>
219 | 			<CardFooter>
220 | 				<div className="flex justify-center w-full border-t pt-4">
221 | 					<p className="text-center text-xs text-neutral-500">
222 | 						built with{" "}
223 | 						<Link
224 | 							href="https://better-auth.com"
225 | 							className="underline"
226 | 							target="_blank"
227 | 						>
228 | 							<span className="dark:text-white/70 cursor-pointer">
229 | 								better-auth.
230 | 							</span>
231 | 						</Link>
232 | 					</p>
233 | 				</div>
234 | 			</CardFooter>
235 | 		</Card>
236 | 	);
237 | }
238 | 
```

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

```typescript
  1 | import { APIError, getSessionFromCtx } from "../../api";
  2 | import {
  3 | 	createAuthEndpoint,
  4 | 	createAuthMiddleware,
  5 | } from "@better-auth/core/api";
  6 | import type {
  7 | 	BetterAuthPlugin,
  8 | 	GenericEndpointContext,
  9 | } from "@better-auth/core";
 10 | import type { InferOptionSchema, Session, User } from "../../types";
 11 | import { parseSetCookieHeader, setSessionCookie } from "../../cookies";
 12 | import { getOrigin } from "../../utils/url";
 13 | import { mergeSchema } from "../../db/schema";
 14 | import type { EndpointContext } from "better-call";
 15 | import { generateId } from "../../utils/id";
 16 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
 17 | import type { AuthContext } from "@better-auth/core";
 18 | import { defineErrorCodes } from "@better-auth/core/utils";
 19 | 
 20 | export interface UserWithAnonymous extends User {
 21 | 	isAnonymous: boolean;
 22 | }
 23 | export interface AnonymousOptions {
 24 | 	/**
 25 | 	 * Configure the domain name of the temporary email
 26 | 	 * address for anonymous users in the database.
 27 | 	 * @default "baseURL"
 28 | 	 */
 29 | 	emailDomainName?: string;
 30 | 	/**
 31 | 	 * A useful hook to run after an anonymous user
 32 | 	 * is about to link their account.
 33 | 	 */
 34 | 	onLinkAccount?: (data: {
 35 | 		anonymousUser: {
 36 | 			user: UserWithAnonymous & Record<string, any>;
 37 | 			session: Session & Record<string, any>;
 38 | 		};
 39 | 		newUser: {
 40 | 			user: User & Record<string, any>;
 41 | 			session: Session & Record<string, any>;
 42 | 		};
 43 | 		ctx: GenericEndpointContext;
 44 | 	}) => Promise<void> | void;
 45 | 	/**
 46 | 	 * Disable deleting the anonymous user after linking
 47 | 	 */
 48 | 	disableDeleteAnonymousUser?: boolean;
 49 | 	/**
 50 | 	 * A hook to generate a name for the anonymous user.
 51 | 	 * Useful if you want to have random names for anonymous users, or if `name` is unique in your database.
 52 | 	 * @returns The name for the anonymous user.
 53 | 	 */
 54 | 	generateName?: (
 55 | 		ctx: EndpointContext<
 56 | 			"/sign-in/anonymous",
 57 | 			{
 58 | 				method: "POST";
 59 | 			},
 60 | 			AuthContext
 61 | 		>,
 62 | 	) => Promise<string> | string;
 63 | 	/**
 64 | 	 * Custom schema for the anonymous plugin
 65 | 	 */
 66 | 	schema?: InferOptionSchema<typeof schema>;
 67 | }
 68 | 
 69 | const schema = {
 70 | 	user: {
 71 | 		fields: {
 72 | 			isAnonymous: {
 73 | 				type: "boolean",
 74 | 				required: false,
 75 | 			},
 76 | 		},
 77 | 	},
 78 | } satisfies BetterAuthPluginDBSchema;
 79 | 
 80 | const ERROR_CODES = defineErrorCodes({
 81 | 	FAILED_TO_CREATE_USER: "Failed to create user",
 82 | 	COULD_NOT_CREATE_SESSION: "Could not create session",
 83 | 	ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY:
 84 | 		"Anonymous users cannot sign in again anonymously",
 85 | });
 86 | 
 87 | export const anonymous = (options?: AnonymousOptions) => {
 88 | 	return {
 89 | 		id: "anonymous",
 90 | 		endpoints: {
 91 | 			signInAnonymous: createAuthEndpoint(
 92 | 				"/sign-in/anonymous",
 93 | 				{
 94 | 					method: "POST",
 95 | 					metadata: {
 96 | 						openapi: {
 97 | 							description: "Sign in anonymously",
 98 | 							responses: {
 99 | 								200: {
100 | 									description: "Sign in anonymously",
101 | 									content: {
102 | 										"application/json": {
103 | 											schema: {
104 | 												type: "object",
105 | 												properties: {
106 | 													user: {
107 | 														$ref: "#/components/schemas/User",
108 | 													},
109 | 													session: {
110 | 														$ref: "#/components/schemas/Session",
111 | 													},
112 | 												},
113 | 											},
114 | 										},
115 | 									},
116 | 								},
117 | 							},
118 | 						},
119 | 					},
120 | 				},
121 | 				async (ctx) => {
122 | 					// If the current request already has a valid anonymous session, we should
123 | 					// reject any further attempts to create another anonymous user. This
124 | 					// prevents an anonymous user from signing in anonymously again while they
125 | 					// are already authenticated.
126 | 					const existingSession = await getSessionFromCtx<{
127 | 						isAnonymous: boolean;
128 | 					}>(ctx, { disableRefresh: true });
129 | 					if (existingSession?.user.isAnonymous) {
130 | 						throw new APIError("BAD_REQUEST", {
131 | 							message:
132 | 								ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
133 | 						});
134 | 					}
135 | 
136 | 					const { emailDomainName = getOrigin(ctx.context.baseURL) } =
137 | 						options || {};
138 | 					const id = generateId();
139 | 					const email = `temp-${id}@${emailDomainName}`;
140 | 					const name = (await options?.generateName?.(ctx)) || "Anonymous";
141 | 					const newUser = await ctx.context.internalAdapter.createUser({
142 | 						email,
143 | 						emailVerified: false,
144 | 						isAnonymous: true,
145 | 						name,
146 | 						createdAt: new Date(),
147 | 						updatedAt: new Date(),
148 | 					});
149 | 					if (!newUser) {
150 | 						throw ctx.error("INTERNAL_SERVER_ERROR", {
151 | 							message: ERROR_CODES.FAILED_TO_CREATE_USER,
152 | 						});
153 | 					}
154 | 					const session = await ctx.context.internalAdapter.createSession(
155 | 						newUser.id,
156 | 					);
157 | 					if (!session) {
158 | 						return ctx.json(null, {
159 | 							status: 400,
160 | 							body: {
161 | 								message: ERROR_CODES.COULD_NOT_CREATE_SESSION,
162 | 							},
163 | 						});
164 | 					}
165 | 					await setSessionCookie(ctx, {
166 | 						session,
167 | 						user: newUser,
168 | 					});
169 | 					return ctx.json({
170 | 						token: session.token,
171 | 						user: {
172 | 							id: newUser.id,
173 | 							email: newUser.email,
174 | 							emailVerified: newUser.emailVerified,
175 | 							name: newUser.name,
176 | 							createdAt: newUser.createdAt,
177 | 							updatedAt: newUser.updatedAt,
178 | 						},
179 | 					});
180 | 				},
181 | 			),
182 | 		},
183 | 		hooks: {
184 | 			after: [
185 | 				{
186 | 					matcher(ctx) {
187 | 						return (
188 | 							ctx.path.startsWith("/sign-in") ||
189 | 							ctx.path.startsWith("/sign-up") ||
190 | 							ctx.path.startsWith("/callback") ||
191 | 							ctx.path.startsWith("/oauth2/callback") ||
192 | 							ctx.path.startsWith("/magic-link/verify") ||
193 | 							ctx.path.startsWith("/email-otp/verify-email") ||
194 | 							ctx.path.startsWith("/one-tap/callback") ||
195 | 							ctx.path.startsWith("/passkey/verify-authentication") ||
196 | 							ctx.path.startsWith("/phone-number/verify")
197 | 						);
198 | 					},
199 | 					handler: createAuthMiddleware(async (ctx) => {
200 | 						const setCookie = ctx.context.responseHeaders?.get("set-cookie");
201 | 
202 | 						/**
203 | 						 * We can consider the user is about to sign in or sign up
204 | 						 * if the response contains a session token.
205 | 						 */
206 | 						const sessionTokenName = ctx.context.authCookies.sessionToken.name;
207 | 						/**
208 | 						 * The user is about to link their account.
209 | 						 */
210 | 						const sessionCookie = parseSetCookieHeader(setCookie || "")
211 | 							.get(sessionTokenName)
212 | 							?.value.split(".")[0]!;
213 | 
214 | 						if (!sessionCookie) {
215 | 							return;
216 | 						}
217 | 						/**
218 | 						 * Make sure the user had an anonymous session.
219 | 						 */
220 | 						const session = await getSessionFromCtx<{ isAnonymous: boolean }>(
221 | 							ctx,
222 | 							{
223 | 								disableRefresh: true,
224 | 							},
225 | 						);
226 | 
227 | 						if (!session || !session.user.isAnonymous) {
228 | 							return;
229 | 						}
230 | 
231 | 						if (ctx.path === "/sign-in/anonymous" && !ctx.context.newSession) {
232 | 							throw new APIError("BAD_REQUEST", {
233 | 								message:
234 | 									ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
235 | 							});
236 | 						}
237 | 						const newSession = ctx.context.newSession;
238 | 						if (!newSession) {
239 | 							return;
240 | 						}
241 | 						// At this point the user is linking their previous anonymous account with a
242 | 						// new credential (email / social). Invoke the provided callback so that the
243 | 						// integrator can perform any additional logic such as transferring data
244 | 						// from the anonymous user to the new user.
245 | 						if (options?.onLinkAccount) {
246 | 							await options?.onLinkAccount?.({
247 | 								anonymousUser: session,
248 | 								newUser: newSession,
249 | 								ctx,
250 | 							});
251 | 						}
252 | 						if (!options?.disableDeleteAnonymousUser) {
253 | 							await ctx.context.internalAdapter.deleteUser(session.user.id);
254 | 						}
255 | 					}),
256 | 				},
257 | 			],
258 | 		},
259 | 		schema: mergeSchema(schema, options?.schema),
260 | 		$ERROR_CODES: ERROR_CODES,
261 | 	} satisfies BetterAuthPlugin;
262 | };
263 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/two-factor/totp/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { APIError } from "better-call";
  2 | import * as z from "zod";
  3 | import { createAuthEndpoint } from "@better-auth/core/api";
  4 | import { sessionMiddleware } from "../../../api";
  5 | import { symmetricDecrypt } from "../../../crypto";
  6 | import type { BackupCodeOptions } from "../backup-codes";
  7 | import { verifyTwoFactor } from "../verify-two-factor";
  8 | import type {
  9 | 	TwoFactorProvider,
 10 | 	TwoFactorTable,
 11 | 	UserWithTwoFactor,
 12 | } from "../types";
 13 | import { setSessionCookie } from "../../../cookies";
 14 | import { TWO_FACTOR_ERROR_CODES } from "../error-code";
 15 | import { createOTP } from "@better-auth/utils/otp";
 16 | import { BASE_ERROR_CODES } from "@better-auth/core/error";
 17 | 
 18 | export type TOTPOptions = {
 19 | 	/**
 20 | 	 * Issuer
 21 | 	 */
 22 | 	issuer?: string;
 23 | 	/**
 24 | 	 * How many digits the otp to be
 25 | 	 *
 26 | 	 * @default 6
 27 | 	 */
 28 | 	digits?: 6 | 8;
 29 | 	/**
 30 | 	 * Period for otp in seconds.
 31 | 	 * @default 30
 32 | 	 */
 33 | 	period?: number;
 34 | 	/**
 35 | 	 * Backup codes configuration
 36 | 	 */
 37 | 	backupCodes?: BackupCodeOptions;
 38 | 	/**
 39 | 	 * Disable totp
 40 | 	 */
 41 | 	disable?: boolean;
 42 | };
 43 | 
 44 | export const totp2fa = (options?: TOTPOptions) => {
 45 | 	const opts = {
 46 | 		...options,
 47 | 		digits: options?.digits || 6,
 48 | 		period: options?.period || 30,
 49 | 	};
 50 | 
 51 | 	const twoFactorTable = "twoFactor";
 52 | 
 53 | 	const generateTOTP = createAuthEndpoint(
 54 | 		"/totp/generate",
 55 | 		{
 56 | 			method: "POST",
 57 | 			body: z.object({
 58 | 				secret: z.string().meta({
 59 | 					description: "The secret to generate the TOTP code",
 60 | 				}),
 61 | 			}),
 62 | 			metadata: {
 63 | 				openapi: {
 64 | 					summary: "Generate TOTP code",
 65 | 					description: "Use this endpoint to generate a TOTP code",
 66 | 					responses: {
 67 | 						200: {
 68 | 							description: "Successful response",
 69 | 							content: {
 70 | 								"application/json": {
 71 | 									schema: {
 72 | 										type: "object",
 73 | 										properties: {
 74 | 											code: {
 75 | 												type: "string",
 76 | 											},
 77 | 										},
 78 | 									},
 79 | 								},
 80 | 							},
 81 | 						},
 82 | 					},
 83 | 				},
 84 | 				SERVER_ONLY: true,
 85 | 			},
 86 | 		},
 87 | 		async (ctx) => {
 88 | 			if (options?.disable) {
 89 | 				ctx.context.logger.error(
 90 | 					"totp isn't configured. please pass totp option on two factor plugin to enable totp",
 91 | 				);
 92 | 				throw new APIError("BAD_REQUEST", {
 93 | 					message: "totp isn't configured",
 94 | 				});
 95 | 			}
 96 | 			const code = await createOTP(ctx.body.secret, {
 97 | 				period: opts.period,
 98 | 				digits: opts.digits,
 99 | 			}).totp();
100 | 			return { code };
101 | 		},
102 | 	);
103 | 
104 | 	const getTOTPURI = createAuthEndpoint(
105 | 		"/two-factor/get-totp-uri",
106 | 		{
107 | 			method: "POST",
108 | 			use: [sessionMiddleware],
109 | 			body: z.object({
110 | 				password: z.string().meta({
111 | 					description: "User password",
112 | 				}),
113 | 			}),
114 | 			metadata: {
115 | 				openapi: {
116 | 					summary: "Get TOTP URI",
117 | 					description: "Use this endpoint to get the TOTP URI",
118 | 					responses: {
119 | 						200: {
120 | 							description: "Successful response",
121 | 							content: {
122 | 								"application/json": {
123 | 									schema: {
124 | 										type: "object",
125 | 										properties: {
126 | 											totpURI: {
127 | 												type: "string",
128 | 											},
129 | 										},
130 | 									},
131 | 								},
132 | 							},
133 | 						},
134 | 					},
135 | 				},
136 | 			},
137 | 		},
138 | 		async (ctx) => {
139 | 			if (options?.disable) {
140 | 				ctx.context.logger.error(
141 | 					"totp isn't configured. please pass totp option on two factor plugin to enable totp",
142 | 				);
143 | 				throw new APIError("BAD_REQUEST", {
144 | 					message: "totp isn't configured",
145 | 				});
146 | 			}
147 | 			const user = ctx.context.session.user as UserWithTwoFactor;
148 | 			const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({
149 | 				model: twoFactorTable,
150 | 				where: [
151 | 					{
152 | 						field: "userId",
153 | 						value: user.id,
154 | 					},
155 | 				],
156 | 			});
157 | 			if (!twoFactor) {
158 | 				throw new APIError("BAD_REQUEST", {
159 | 					message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
160 | 				});
161 | 			}
162 | 			const secret = await symmetricDecrypt({
163 | 				key: ctx.context.secret,
164 | 				data: twoFactor.secret,
165 | 			});
166 | 			await ctx.context.password.checkPassword(user.id, ctx);
167 | 			const totpURI = createOTP(secret, {
168 | 				digits: opts.digits,
169 | 				period: opts.period,
170 | 			}).url(options?.issuer || ctx.context.appName, user.email);
171 | 			return {
172 | 				totpURI,
173 | 			};
174 | 		},
175 | 	);
176 | 
177 | 	const verifyTOTP = createAuthEndpoint(
178 | 		"/two-factor/verify-totp",
179 | 		{
180 | 			method: "POST",
181 | 			body: z.object({
182 | 				code: z.string().meta({
183 | 					description: 'The otp code to verify. Eg: "012345"',
184 | 				}),
185 | 				/**
186 | 				 * if true, the device will be trusted
187 | 				 * for 30 days. It'll be refreshed on
188 | 				 * every sign in request within this time.
189 | 				 */
190 | 				trustDevice: z
191 | 					.boolean()
192 | 					.meta({
193 | 						description:
194 | 							"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
195 | 					})
196 | 					.optional(),
197 | 			}),
198 | 			metadata: {
199 | 				openapi: {
200 | 					summary: "Verify two factor TOTP",
201 | 					description: "Verify two factor TOTP",
202 | 					responses: {
203 | 						200: {
204 | 							description: "Successful response",
205 | 							content: {
206 | 								"application/json": {
207 | 									schema: {
208 | 										type: "object",
209 | 										properties: {
210 | 											status: {
211 | 												type: "boolean",
212 | 											},
213 | 										},
214 | 									},
215 | 								},
216 | 							},
217 | 						},
218 | 					},
219 | 				},
220 | 			},
221 | 		},
222 | 		async (ctx) => {
223 | 			if (options?.disable) {
224 | 				ctx.context.logger.error(
225 | 					"totp isn't configured. please pass totp option on two factor plugin to enable totp",
226 | 				);
227 | 				throw new APIError("BAD_REQUEST", {
228 | 					message: "totp isn't configured",
229 | 				});
230 | 			}
231 | 			const { session, valid, invalid } = await verifyTwoFactor(ctx);
232 | 			const user = session.user as UserWithTwoFactor;
233 | 			const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({
234 | 				model: twoFactorTable,
235 | 				where: [
236 | 					{
237 | 						field: "userId",
238 | 						value: user.id,
239 | 					},
240 | 				],
241 | 			});
242 | 
243 | 			if (!twoFactor) {
244 | 				throw new APIError("BAD_REQUEST", {
245 | 					message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED,
246 | 				});
247 | 			}
248 | 			const decrypted = await symmetricDecrypt({
249 | 				key: ctx.context.secret,
250 | 				data: twoFactor.secret,
251 | 			});
252 | 			const status = await createOTP(decrypted, {
253 | 				period: opts.period,
254 | 				digits: opts.digits,
255 | 			}).verify(ctx.body.code);
256 | 			if (!status) {
257 | 				return invalid("INVALID_CODE");
258 | 			}
259 | 
260 | 			if (!user.twoFactorEnabled) {
261 | 				if (!session.session) {
262 | 					throw new APIError("BAD_REQUEST", {
263 | 						message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
264 | 					});
265 | 				}
266 | 				const updatedUser = await ctx.context.internalAdapter.updateUser(
267 | 					user.id,
268 | 					{
269 | 						twoFactorEnabled: true,
270 | 					},
271 | 				);
272 | 				const newSession = await ctx.context.internalAdapter
273 | 					.createSession(user.id, false, session.session)
274 | 					.catch((e) => {
275 | 						throw e;
276 | 					});
277 | 
278 | 				await ctx.context.internalAdapter.deleteSession(session.session.token);
279 | 				await setSessionCookie(ctx, {
280 | 					session: newSession,
281 | 					user: updatedUser,
282 | 				});
283 | 			}
284 | 			return valid(ctx);
285 | 		},
286 | 	);
287 | 
288 | 	return {
289 | 		id: "totp",
290 | 		endpoints: {
291 | 			/**
292 | 			 * ### Endpoint
293 | 			 *
294 | 			 * POST `/totp/generate`
295 | 			 *
296 | 			 * ### API Methods
297 | 			 *
298 | 			 * **server:**
299 | 			 * `auth.api.generateTOTP`
300 | 			 *
301 | 			 * **client:**
302 | 			 * `authClient.totp.generate`
303 | 			 *
304 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/totp#api-method-totp-generate)
305 | 			 */
306 | 			generateTOTP: generateTOTP,
307 | 			/**
308 | 			 * ### Endpoint
309 | 			 *
310 | 			 * POST `/two-factor/get-totp-uri`
311 | 			 *
312 | 			 * ### API Methods
313 | 			 *
314 | 			 * **server:**
315 | 			 * `auth.api.getTOTPURI`
316 | 			 *
317 | 			 * **client:**
318 | 			 * `authClient.twoFactor.getTotpUri`
319 | 			 *
320 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/two-factor#api-method-two-factor-get-totp-uri)
321 | 			 */
322 | 			getTOTPURI: getTOTPURI,
323 | 			verifyTOTP,
324 | 		},
325 | 	} satisfies TwoFactorProvider;
326 | };
327 | 
```

--------------------------------------------------------------------------------
/docs/app/blog/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { blogs } from "@/lib/source";
  2 | import { notFound } from "next/navigation";
  3 | import { absoluteUrl, formatDate } from "@/lib/utils";
  4 | import DatabaseTable from "@/components/mdx/database-tables";
  5 | import { cn } from "@/lib/utils";
  6 | import { Step, Steps } from "fumadocs-ui/components/steps";
  7 | import { Tab, Tabs } from "fumadocs-ui/components/tabs";
  8 | import { GenerateSecret } from "@/components/generate-secret";
  9 | import { AnimatePresence } from "@/components/ui/fade-in";
 10 | import { TypeTable } from "fumadocs-ui/components/type-table";
 11 | import { Features } from "@/components/blocks/features";
 12 | import { ForkButton } from "@/components/fork-button";
 13 | import Link from "next/link";
 14 | import defaultMdxComponents from "fumadocs-ui/mdx";
 15 | import { File, Folder, Files } from "fumadocs-ui/components/files";
 16 | import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
 17 | import { Pre } from "fumadocs-ui/components/codeblock";
 18 | import { Glow } from "../_components/default-changelog";
 19 | import { XIcon } from "../_components/icons";
 20 | import { StarField } from "../_components/stat-field";
 21 | import { BlogPage } from "../_components/blog-list";
 22 | import { Callout } from "@/components/ui/callout";
 23 | import { ArrowLeftIcon, ExternalLink } from "lucide-react";
 24 | import { Support } from "../_components/support";
 25 | import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
 26 | 
 27 | const metaTitle = "Blogs";
 28 | const metaDescription = "Latest changes , fixes and updates.";
 29 | const ogImage = "https://better-auth.com/release-og/changelog-og.png";
 30 | 
 31 | export default async function Page({
 32 | 	params,
 33 | }: {
 34 | 	params: Promise<{ slug?: string[] }>;
 35 | }) {
 36 | 	const { slug } = await params;
 37 | 	if (!slug) {
 38 | 		return <BlogPage />;
 39 | 	}
 40 | 	const page = blogs.getPage(slug);
 41 | 	if (!page) {
 42 | 		notFound();
 43 | 	}
 44 | 	const MDX = page.data?.body;
 45 | 	const { title, description, date } = page.data;
 46 | 	return (
 47 | 		<div className="relative min-h-screen">
 48 | 			<div className="pointer-events-none absolute inset-0 -z-10">
 49 | 				<StarField className="top-1/3 left-1/2 -translate-x-1/2" />
 50 | 				<Glow />
 51 | 			</div>
 52 | 			<div className="relative mx-auto max-w-3xl px-4 md:px-0 pb-24 pt-12">
 53 | 				<h1 className="text-center text-3xl md:text-5xl font-semibold tracking-tighter">
 54 | 					{title}
 55 | 				</h1>
 56 | 				{description && (
 57 | 					<p className="mt-3 text-center text-muted-foreground">
 58 | 						{description}
 59 | 					</p>
 60 | 				)}
 61 | 				<div className="my-2 flex items-center justify-center gap-3">
 62 | 					<div>
 63 | 						<Avatar>
 64 | 							<AvatarImage
 65 | 								src={page.data?.author?.avatar}
 66 | 								alt={page.data?.author?.name ?? "Author"}
 67 | 							/>
 68 | 							<AvatarFallback>
 69 | 								{page.data?.author?.name?.charAt(0)?.toUpperCase() ?? ""}
 70 | 							</AvatarFallback>
 71 | 						</Avatar>
 72 | 					</div>
 73 | 					<div className="flex items-center gap-2 text-sm text-muted-foreground">
 74 | 						{page.data?.author?.name && (
 75 | 							<span className="font-medium text-foreground">
 76 | 								{page.data.author.name}
 77 | 							</span>
 78 | 						)}
 79 | 						{page.data?.author?.twitter && (
 80 | 							<>
 81 | 								<span>·</span>
 82 | 								<a
 83 | 									href={`https://x.com/${page.data.author.twitter}`}
 84 | 									target="_blank"
 85 | 									rel="noreferrer noopener"
 86 | 									className="inline-flex items-center gap-1 underline decoration-dashed"
 87 | 								>
 88 | 									<XIcon className="size-3" />@{page.data.author.twitter}
 89 | 								</a>
 90 | 							</>
 91 | 						)}
 92 | 						{date && (
 93 | 							<>
 94 | 								<span>·</span>
 95 | 								<time dateTime={String(date)}>{formatDate(date)}</time>
 96 | 							</>
 97 | 						)}
 98 | 					</div>
 99 | 				</div>
100 | 				<div className="w-full flex items-center gap-2 my-4 mb-8">
101 | 					<div className="flex items-center gap-2 opacity-80">
102 | 						<ArrowLeftIcon className="size-4" />
103 | 						<Link href="/blog" className="">
104 | 							Blogs
105 | 						</Link>
106 | 					</div>
107 | 					<hr className="h-1 w-full opacity-80" />
108 | 				</div>
109 | 
110 | 				<article className="prose prose-neutral dark:prose-invert mx-auto max-w-3xl px-4 md:px-0">
111 | 					<MDX
112 | 						components={{
113 | 							...defaultMdxComponents,
114 | 							a: ({ className, href, children, ...props }: any) => {
115 | 								const isExternal =
116 | 									typeof href === "string" && /^(https?:)?\/\//.test(href);
117 | 								const classes = cn(
118 | 									"inline-flex items-center gap-1 font-medium underline decoration-dashed",
119 | 									className,
120 | 								);
121 | 								if (isExternal) {
122 | 									return (
123 | 										<a
124 | 											className={classes}
125 | 											href={href}
126 | 											target="_blank"
127 | 											rel="noreferrer noopener"
128 | 											{...props}
129 | 										>
130 | 											{children}
131 | 											<ExternalLink className="ms-0.5 inline size-[0.9em] text-fd-muted-foreground" />
132 | 										</a>
133 | 									);
134 | 								}
135 | 								return (
136 | 									<Link className={classes} href={href} {...(props as any)}>
137 | 										{children}
138 | 									</Link>
139 | 								);
140 | 							},
141 | 							Link: ({ className, href, children, ...props }: any) => {
142 | 								const isExternal =
143 | 									typeof href === "string" && /^(https?:)?\/\//.test(href);
144 | 								const classes = cn(
145 | 									"inline-flex items-center gap-1 font-medium underline decoration-dashed",
146 | 									className,
147 | 								);
148 | 								if (isExternal) {
149 | 									return (
150 | 										<a
151 | 											className={classes}
152 | 											href={href}
153 | 											target="_blank"
154 | 											rel="noreferrer noopener"
155 | 											{...props}
156 | 										>
157 | 											{children}
158 | 											<ExternalLink className="ms-0.5 inline size-[0.9em] text-fd-muted-foreground" />
159 | 										</a>
160 | 									);
161 | 								}
162 | 								return (
163 | 									<Link className={classes} href={href} {...(props as any)}>
164 | 										{children}
165 | 									</Link>
166 | 								);
167 | 							},
168 | 							Step,
169 | 							Steps,
170 | 							File,
171 | 							Folder,
172 | 							Files,
173 | 							Tab,
174 | 							Tabs,
175 | 							Pre: Pre,
176 | 							GenerateSecret,
177 | 							AnimatePresence,
178 | 							TypeTable,
179 | 							Features,
180 | 							ForkButton,
181 | 							DatabaseTable,
182 | 							Accordion,
183 | 							Accordions,
184 | 							Callout: ({
185 | 								children,
186 | 								type,
187 | 								...props
188 | 							}: {
189 | 								children: React.ReactNode;
190 | 								type?: "info" | "warn" | "error" | "success" | "warning";
191 | 								[key: string]: any;
192 | 							}) => (
193 | 								<Callout type={type} {...props}>
194 | 									{children}
195 | 								</Callout>
196 | 							),
197 | 							Support,
198 | 						}}
199 | 					/>
200 | 				</article>
201 | 			</div>
202 | 		</div>
203 | 	);
204 | }
205 | 
206 | export async function generateMetadata({
207 | 	params,
208 | }: {
209 | 	params: Promise<{ slug?: string[] }>;
210 | }) {
211 | 	const { slug } = await params;
212 | 	if (!slug) {
213 | 		return {
214 | 			metadataBase: new URL("https://better-auth.com/blogs"),
215 | 			title: metaTitle,
216 | 			description: metaDescription,
217 | 			openGraph: {
218 | 				title: metaTitle,
219 | 				description: metaDescription,
220 | 				images: [
221 | 					{
222 | 						url: ogImage,
223 | 					},
224 | 				],
225 | 				url: "https://better-auth.com/blogs",
226 | 			},
227 | 			twitter: {
228 | 				card: "summary_large_image",
229 | 				title: metaTitle,
230 | 				description: metaDescription,
231 | 				images: [ogImage],
232 | 			},
233 | 		};
234 | 	}
235 | 	const page = blogs.getPage(slug);
236 | 	if (page == null) notFound();
237 | 	const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL;
238 | 	const url = new URL(
239 | 		`${baseUrl?.startsWith("http") ? baseUrl : `https://${baseUrl}`}${
240 | 			page.data?.image
241 | 		}`,
242 | 	);
243 | 	const { title, description } = page.data;
244 | 
245 | 	return {
246 | 		title,
247 | 		description,
248 | 		openGraph: {
249 | 			title,
250 | 			description,
251 | 			type: "website",
252 | 			url: absoluteUrl(`blog/${slug.join("/")}`),
253 | 			images: [
254 | 				{
255 | 					url: url.toString(),
256 | 					width: 1200,
257 | 					height: 630,
258 | 					alt: title,
259 | 				},
260 | 			],
261 | 		},
262 | 		twitter: {
263 | 			card: "summary_large_image",
264 | 			title,
265 | 			description,
266 | 			images: [url.toString()],
267 | 		},
268 | 	};
269 | }
270 | 
271 | export function generateStaticParams() {
272 | 	return blogs.generateParams();
273 | }
274 | 
```

--------------------------------------------------------------------------------
/docs/app/v1/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { ArrowRight } from "lucide-react";
  2 | import { Button } from "@/components/ui/button";
  3 | import { BackgroundLines } from "./bg-line";
  4 | import Link from "next/link";
  5 | import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
  6 | import { Metadata } from "next";
  7 | 
  8 | export const metadata: Metadata = {
  9 | 	title: "V1.0 Release",
 10 | 	description: "Better Auth V1.0 release notes",
 11 | 	openGraph: {
 12 | 		images: "https://better-auth.com/v1-og.png",
 13 | 		title: "V1.0 Release",
 14 | 		description: "Better Auth V1.0 release notes",
 15 | 		url: "https://better-auth.com/v1",
 16 | 		type: "article",
 17 | 		siteName: "Better Auth",
 18 | 	},
 19 | 	twitter: {
 20 | 		images: "https://better-auth.com/v1-og.png",
 21 | 		card: "summary_large_image",
 22 | 		site: "@better_auth",
 23 | 		creator: "@better_auth",
 24 | 		title: "V1.0 Release",
 25 | 		description: "Better Auth V1.0 release notes",
 26 | 	},
 27 | };
 28 | 
 29 | export default function V1Ship() {
 30 | 	return (
 31 | 		<div className="min-h-screen bg-transparent overflow-hidden">
 32 | 			<div className="h-[50vh] bg-transparent/10 relative">
 33 | 				<BackgroundLines>
 34 | 					<div className="absolute bottom-1/3 left-1/2 transform -translate-x-1/2 text-center">
 35 | 						<h1 className="text-5xl mb-4">V1.0 - nov.22</h1>
 36 | 						<p className="text-lg text-gray-400 max-w-xl mx-auto">
 37 | 							We are excited to announce the Better Auth V1.0 release.
 38 | 						</p>
 39 | 					</div>
 40 | 				</BackgroundLines>
 41 | 			</div>
 42 | 
 43 | 			<div className="relative py-24">
 44 | 				<div className="absolute inset-0 z-0">
 45 | 					<div className="grid grid-cols-12 h-full">
 46 | 						{Array(12)
 47 | 							.fill(null)
 48 | 							.map((_, i) => (
 49 | 								<div
 50 | 									key={i}
 51 | 									className="border-l border-dashed border-stone-100 dark:border-white/10 h-full"
 52 | 								/>
 53 | 							))}
 54 | 					</div>
 55 | 					<div className="grid grid-rows-12 w-full absolute top-0">
 56 | 						{Array(12)
 57 | 							.fill(null)
 58 | 							.map((_, i) => (
 59 | 								<div
 60 | 									key={i}
 61 | 									className="border-t border-dashed border-stone-100 dark:border-stone-900/60 w-full"
 62 | 								/>
 63 | 							))}
 64 | 					</div>
 65 | 				</div>
 66 | 				<div className="max-w-6xl mx-auto px-6 relative z-10">
 67 | 					<h2 className="text-3xl font-bold mb-12 font-geist text-center">
 68 | 						What does V1 means?
 69 | 					</h2>
 70 | 					<p>
 71 | 						Since introducing Better Auth, the community's excitement has been
 72 | 						incredibly motivating—thank you! <br /> <br />
 73 | 						V1 is an important milestone, but it simply means we believe you can
 74 | 						use it in production and that we'll strive to keep the APIs stable
 75 | 						until the next major version. However, we'll continue improving,
 76 | 						adding new features, and fixing bugs at the same pace as before.
 77 | 						<br /> <br />
 78 | 						If you were using Better Auth for production, we recommend updating
 79 | 						to V1 as soon as possible. There are some breaking changes, feel
 80 | 						free to join us on{" "}
 81 | 						<Link href="https://discord.gg/better-auth">Discord</Link>, and
 82 | 						we'll gladly assist.
 83 | 					</p>
 84 | 				</div>
 85 | 			</div>
 86 | 
 87 | 			<ReleaseRelated />
 88 | 
 89 | 			<div className="border-t border-white/10">
 90 | 				<div className="max-w-4xl mx-auto px-6 py-24">
 91 | 					<h2 className="text-3xl font-bold mb-12 font-geist">Changelog</h2>
 92 | 					<div className="space-y-8">
 93 | 						<ChangelogItem
 94 | 							version="1.0.0"
 95 | 							date="2024"
 96 | 							changes={[
 97 | 								"feat: Open API Docs",
 98 | 								"docs: Sign In Box Builder",
 99 | 								"feat: default memory adapter. If no database is provided, it will use memory adapter",
100 | 								"feat: New server only endpoints for Organization and Two Factor plugins",
101 | 								"refactor: all core tables now have `createdAt` and `updatedAt` fields",
102 | 								"refactor: accounts now store `expiresAt` for both refresh and access tokens",
103 | 								"feat: Email OTP forget password flow",
104 | 								"docs: NextAuth.js migration guide",
105 | 								"feat: sensitive endpoints now check for fresh tokens",
106 | 								"feat: two-factor now have different interface for redirect and callback",
107 | 								"and a lot more bug fixes and improvements...",
108 | 							]}
109 | 						/>
110 | 					</div>
111 | 				</div>
112 | 			</div>
113 | 		</div>
114 | 	);
115 | }
116 | 
117 | function ReleaseRelated() {
118 | 	return (
119 | 		<div className="relative dark:bg-transparent/10 bg-zinc-100 border-b-2 border-white/10 rounded-none py-24">
120 | 			<div className="absolute inset-0 z-0">
121 | 				<div className="grid grid-rows-12 w-full absolute top-0">
122 | 					{Array(12)
123 | 						.fill(null)
124 | 						.map((_, i) => (
125 | 							<div
126 | 								key={i}
127 | 								className="border-t border-dashed border-white/10 w-full"
128 | 							/>
129 | 						))}
130 | 				</div>
131 | 			</div>
132 | 			<div className="max-w-6xl mx-auto px-6 relative z-10">
133 | 				<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
134 | 					<div>
135 | 						<h3 className="text-xl font-semibold mb-4">Install Latest</h3>
136 | 						<div className="dark:bg-white/5 bg-black/10 rounded-lg p-4 mb-2">
137 | 							<code className="text-sm font-mono">
138 | 								npm i better-auth@latest
139 | 							</code>
140 | 						</div>
141 | 						<p className="text-sm text-gray-400">
142 | 							Get the latest{" "}
143 | 							<a href="#" className="underline">
144 | 								Node.js and npm
145 | 							</a>
146 | 							.
147 | 						</p>
148 | 					</div>
149 | 					<div>
150 | 						<h3 className="text-xl font-semibold mb-4">Adopt the new Schema</h3>
151 | 						<div className="dark:bg-white/5 bg-black/10 rounded-lg p-4 mb-2">
152 | 							<code className="text-sm font-mono ">
153 | 								pnpx @better-auth/cli migrate
154 | 								<br />
155 | 							</code>
156 | 						</div>
157 | 						<p className="text-sm text-gray-400">
158 | 							Ensure you have the latest{" "}
159 | 							<code className="text-xs dark:bg-white/5 bg-black/10 px-1 py-0.5 rounded">
160 | 								schema required
161 | 							</code>{" "}
162 | 							by Better Auth.
163 | 							<code className="text-xs dark:bg-white/5 bg-black/10 px-1 py-0.5 rounded">
164 | 								You can also
165 | 							</code>{" "}
166 | 							add them manually. Read the{" "}
167 | 							<a
168 | 								href="/docs/concepts/database#core-schema"
169 | 								className="underline"
170 | 							>
171 | 								Core Schema
172 | 							</a>{" "}
173 | 							for full instructions.
174 | 						</p>
175 | 					</div>
176 | 					<div>
177 | 						<h3 className="text-xl font-semibold mb-4">
178 | 							Check out the change log, the new UI Builder, OpenAPI Docs, and
179 | 							more
180 | 						</h3>
181 | 						<p className="text-sm text-gray-400 mb-4">
182 | 							We have some exciting new features and updates that you should
183 | 							check out.
184 | 						</p>
185 | 						<Link
186 | 							className="w-full"
187 | 							href="https://github.com/better-auth/better-auth"
188 | 						>
189 | 							<Button variant="outline" className="w-full justify-between">
190 | 								<div className="flex items-center gap-2">
191 | 									<GitHubLogoIcon fontSize={10} />
192 | 									Star on GitHub
193 | 								</div>
194 | 								<ArrowRight className="w-4 h-4" />
195 | 							</Button>
196 | 						</Link>
197 | 						<Link className="w-full" href="https://discord.gg/better-auth">
198 | 							<Button
199 | 								variant="outline"
200 | 								className="w-full justify-between border-t-0"
201 | 							>
202 | 								<div className="flex items-center gap-2">
203 | 									<DiscordLogoIcon />
204 | 									Join Discord
205 | 								</div>
206 | 								<ArrowRight className="w-4 h-4" />
207 | 							</Button>
208 | 						</Link>
209 | 					</div>
210 | 				</div>
211 | 			</div>
212 | 		</div>
213 | 	);
214 | }
215 | 
216 | function ChangelogItem({
217 | 	version,
218 | 	date,
219 | 	changes,
220 | }: {
221 | 	version: string;
222 | 	date: string;
223 | 	changes: string[];
224 | }) {
225 | 	return (
226 | 		<div className="border-l-2 border-white/10 pl-6 relative">
227 | 			<div className="absolute w-3 h-3 bg-white rounded-full -left-[7px] top-2" />
228 | 			<div className="flex items-center gap-4 mb-4">
229 | 				<h3 className="text-xl font-bold font-geist">{version}</h3>
230 | 				<span className="text-sm text-gray-400">{date}</span>
231 | 			</div>
232 | 			<ul className="space-y-3">
233 | 				{changes.map((change, i) => (
234 | 					<li key={i} className="text-gray-400">
235 | 						{change}
236 | 					</li>
237 | 				))}
238 | 			</ul>
239 | 		</div>
240 | 	);
241 | }
242 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/oidc-provider/authorize.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { APIError } from "better-call";
  2 | import { getSessionFromCtx } from "../../api";
  3 | import type { AuthorizationQuery, OIDCOptions } from "./types";
  4 | import { generateRandomString } from "../../crypto";
  5 | import { getClient } from "./index";
  6 | import type { GenericEndpointContext } from "@better-auth/core";
  7 | 
  8 | function formatErrorURL(url: string, error: string, description: string) {
  9 | 	return `${
 10 | 		url.includes("?") ? "&" : "?"
 11 | 	}error=${error}&error_description=${description}`;
 12 | }
 13 | 
 14 | function getErrorURL(
 15 | 	ctx: GenericEndpointContext,
 16 | 	error: string,
 17 | 	description: string,
 18 | ) {
 19 | 	const baseURL =
 20 | 		ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
 21 | 	const formattedURL = formatErrorURL(baseURL, error, description);
 22 | 	return formattedURL;
 23 | }
 24 | 
 25 | export async function authorize(
 26 | 	ctx: GenericEndpointContext,
 27 | 	options: OIDCOptions,
 28 | ) {
 29 | 	const handleRedirect = (url: string) => {
 30 | 		const fromFetch = ctx.request?.headers.get("sec-fetch-mode") === "cors";
 31 | 		if (fromFetch) {
 32 | 			return ctx.json({
 33 | 				redirect: true,
 34 | 				url,
 35 | 			});
 36 | 		} else {
 37 | 			throw ctx.redirect(url);
 38 | 		}
 39 | 	};
 40 | 
 41 | 	const opts = {
 42 | 		codeExpiresIn: 600,
 43 | 		defaultScope: "openid",
 44 | 		...options,
 45 | 		scopes: [
 46 | 			"openid",
 47 | 			"profile",
 48 | 			"email",
 49 | 			"offline_access",
 50 | 			...(options?.scopes || []),
 51 | 		],
 52 | 	};
 53 | 	if (!ctx.request) {
 54 | 		throw new APIError("UNAUTHORIZED", {
 55 | 			error_description: "request not found",
 56 | 			error: "invalid_request",
 57 | 		});
 58 | 	}
 59 | 	const session = await getSessionFromCtx(ctx);
 60 | 	if (!session) {
 61 | 		/**
 62 | 		 * If the user is not logged in, we need to redirect them to the
 63 | 		 * login page.
 64 | 		 */
 65 | 		await ctx.setSignedCookie(
 66 | 			"oidc_login_prompt",
 67 | 			JSON.stringify(ctx.query),
 68 | 			ctx.context.secret,
 69 | 			{
 70 | 				maxAge: 600,
 71 | 				path: "/",
 72 | 				sameSite: "lax",
 73 | 			},
 74 | 		);
 75 | 		const queryFromURL = ctx.request.url?.split("?")[1]!;
 76 | 		return handleRedirect(`${options.loginPage}?${queryFromURL}`);
 77 | 	}
 78 | 
 79 | 	const query = ctx.query as AuthorizationQuery;
 80 | 	if (!query.client_id) {
 81 | 		const errorURL = getErrorURL(
 82 | 			ctx,
 83 | 			"invalid_client",
 84 | 			"client_id is required",
 85 | 		);
 86 | 		throw ctx.redirect(errorURL);
 87 | 	}
 88 | 
 89 | 	if (!query.response_type) {
 90 | 		const errorURL = getErrorURL(
 91 | 			ctx,
 92 | 			"invalid_request",
 93 | 			"response_type is required",
 94 | 		);
 95 | 		throw ctx.redirect(
 96 | 			getErrorURL(ctx, "invalid_request", "response_type is required"),
 97 | 		);
 98 | 	}
 99 | 
100 | 	const client = await getClient(
101 | 		ctx.query.client_id,
102 | 		ctx.context.adapter,
103 | 		options.trustedClients || [],
104 | 	);
105 | 	if (!client) {
106 | 		const errorURL = getErrorURL(
107 | 			ctx,
108 | 			"invalid_client",
109 | 			"client_id is required",
110 | 		);
111 | 		throw ctx.redirect(errorURL);
112 | 	}
113 | 	const redirectURI = client.redirectURLs.find(
114 | 		(url) => url === ctx.query.redirect_uri,
115 | 	);
116 | 
117 | 	if (!redirectURI || !query.redirect_uri) {
118 | 		/**
119 | 		 * show UI error here warning the user that the redirect URI is invalid
120 | 		 */
121 | 		throw new APIError("BAD_REQUEST", {
122 | 			message: "Invalid redirect URI",
123 | 		});
124 | 	}
125 | 	if (client.disabled) {
126 | 		const errorURL = getErrorURL(ctx, "client_disabled", "client is disabled");
127 | 		throw ctx.redirect(errorURL);
128 | 	}
129 | 
130 | 	if (query.response_type !== "code") {
131 | 		const errorURL = getErrorURL(
132 | 			ctx,
133 | 			"unsupported_response_type",
134 | 			"unsupported response type",
135 | 		);
136 | 		throw ctx.redirect(errorURL);
137 | 	}
138 | 
139 | 	const requestScope =
140 | 		query.scope?.split(" ").filter((s) => s) || opts.defaultScope.split(" ");
141 | 	const invalidScopes = requestScope.filter((scope) => {
142 | 		return !opts.scopes.includes(scope);
143 | 	});
144 | 	if (invalidScopes.length) {
145 | 		return handleRedirect(
146 | 			formatErrorURL(
147 | 				query.redirect_uri,
148 | 				"invalid_scope",
149 | 				`The following scopes are invalid: ${invalidScopes.join(", ")}`,
150 | 			),
151 | 		);
152 | 	}
153 | 
154 | 	if (
155 | 		(!query.code_challenge || !query.code_challenge_method) &&
156 | 		options.requirePKCE
157 | 	) {
158 | 		return handleRedirect(
159 | 			formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required"),
160 | 		);
161 | 	}
162 | 
163 | 	if (!query.code_challenge_method) {
164 | 		query.code_challenge_method = "plain";
165 | 	}
166 | 
167 | 	if (
168 | 		![
169 | 			"s256",
170 | 			options.allowPlainCodeChallengeMethod ? "plain" : "s256",
171 | 		].includes(query.code_challenge_method?.toLowerCase() || "")
172 | 	) {
173 | 		return handleRedirect(
174 | 			formatErrorURL(
175 | 				query.redirect_uri,
176 | 				"invalid_request",
177 | 				"invalid code_challenge method",
178 | 			),
179 | 		);
180 | 	}
181 | 
182 | 	const code = generateRandomString(32, "a-z", "A-Z", "0-9");
183 | 	const codeExpiresInMs = opts.codeExpiresIn * 1000;
184 | 	const expiresAt = new Date(Date.now() + codeExpiresInMs);
185 | 
186 | 	// Determine if consent is required
187 | 	// Consent is ALWAYS required unless:
188 | 	// 1. The client is trusted (skipConsent = true)
189 | 	// 2. The user has already consented and prompt is not "consent"
190 | 	const skipConsentForTrustedClient = client.skipConsent;
191 | 	const hasAlreadyConsented = await ctx.context.adapter
192 | 		.findOne<{
193 | 			consentGiven: boolean;
194 | 		}>({
195 | 			model: "oauthConsent",
196 | 			where: [
197 | 				{
198 | 					field: "clientId",
199 | 					value: client.clientId,
200 | 				},
201 | 				{
202 | 					field: "userId",
203 | 					value: session.user.id,
204 | 				},
205 | 			],
206 | 		})
207 | 		.then((res) => !!res?.consentGiven);
208 | 
209 | 	const requireConsent =
210 | 		!skipConsentForTrustedClient &&
211 | 		(!hasAlreadyConsented || query.prompt === "consent");
212 | 
213 | 	try {
214 | 		/**
215 | 		 * Save the code in the database
216 | 		 */
217 | 		await ctx.context.internalAdapter.createVerificationValue({
218 | 			value: JSON.stringify({
219 | 				clientId: client.clientId,
220 | 				redirectURI: query.redirect_uri,
221 | 				scope: requestScope,
222 | 				userId: session.user.id,
223 | 				authTime: new Date(session.session.createdAt).getTime(),
224 | 				/**
225 | 				 * Consent is required per OIDC spec unless:
226 | 				 * 1. Client is trusted (skipConsent = true)
227 | 				 * 2. User has already consented (and prompt is not "consent")
228 | 				 *
229 | 				 * When consent is required, the code needs to be treated as a
230 | 				 * consent request. Once the user consents, the code will be
231 | 				 * updated with the actual authorization code.
232 | 				 */
233 | 				requireConsent,
234 | 				state: requireConsent ? query.state : null,
235 | 				codeChallenge: query.code_challenge,
236 | 				codeChallengeMethod: query.code_challenge_method,
237 | 				nonce: query.nonce,
238 | 			}),
239 | 			identifier: code,
240 | 			expiresAt,
241 | 		});
242 | 	} catch (e) {
243 | 		return handleRedirect(
244 | 			formatErrorURL(
245 | 				query.redirect_uri,
246 | 				"server_error",
247 | 				"An error occurred while processing the request",
248 | 			),
249 | 		);
250 | 	}
251 | 
252 | 	// If consent is not required, redirect with the code immediately
253 | 	if (!requireConsent) {
254 | 		const redirectURIWithCode = new URL(redirectURI);
255 | 		redirectURIWithCode.searchParams.set("code", code);
256 | 		redirectURIWithCode.searchParams.set("state", ctx.query.state);
257 | 		return handleRedirect(redirectURIWithCode.toString());
258 | 	}
259 | 
260 | 	// Consent is required - redirect to consent page or show consent HTML
261 | 
262 | 	if (options?.consentPage) {
263 | 		// Set cookie to support cookie-based consent flows
264 | 		await ctx.setSignedCookie("oidc_consent_prompt", code, ctx.context.secret, {
265 | 			maxAge: 600,
266 | 			path: "/",
267 | 			sameSite: "lax",
268 | 		});
269 | 
270 | 		// Pass the consent code as a URL parameter to support URL-based consent flows
271 | 		const urlParams = new URLSearchParams();
272 | 		urlParams.set("consent_code", code);
273 | 		urlParams.set("client_id", client.clientId);
274 | 		urlParams.set("scope", requestScope.join(" "));
275 | 		const consentURI = `${options.consentPage}?${urlParams.toString()}`;
276 | 
277 | 		return handleRedirect(consentURI);
278 | 	}
279 | 	const htmlFn = options?.getConsentHTML;
280 | 
281 | 	if (!htmlFn) {
282 | 		throw new APIError("INTERNAL_SERVER_ERROR", {
283 | 			message: "No consent page provided",
284 | 		});
285 | 	}
286 | 
287 | 	return new Response(
288 | 		htmlFn({
289 | 			scopes: requestScope,
290 | 			clientMetadata: client.metadata,
291 | 			clientIcon: client?.icon,
292 | 			clientId: client.clientId,
293 | 			clientName: client.name,
294 | 			code,
295 | 		}),
296 | 		{
297 | 			headers: {
298 | 				"content-type": "text/html",
299 | 			},
300 | 		},
301 | 	);
302 | }
303 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/prisma-adapter/prisma-adapter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BetterAuthError } from "@better-auth/core/error";
  2 | import type { BetterAuthOptions } from "@better-auth/core";
  3 | import {
  4 | 	createAdapterFactory,
  5 | 	type AdapterFactoryOptions,
  6 | 	type AdapterFactoryCustomizeAdapterCreator,
  7 | } from "../adapter-factory";
  8 | import type {
  9 | 	DBAdapterDebugLogOption,
 10 | 	DBAdapter,
 11 | 	Where,
 12 | } from "@better-auth/core/db/adapter";
 13 | 
 14 | export interface PrismaConfig {
 15 | 	/**
 16 | 	 * Database provider.
 17 | 	 */
 18 | 	provider:
 19 | 		| "sqlite"
 20 | 		| "cockroachdb"
 21 | 		| "mysql"
 22 | 		| "postgresql"
 23 | 		| "sqlserver"
 24 | 		| "mongodb";
 25 | 
 26 | 	/**
 27 | 	 * Enable debug logs for the adapter
 28 | 	 *
 29 | 	 * @default false
 30 | 	 */
 31 | 	debugLogs?: DBAdapterDebugLogOption;
 32 | 
 33 | 	/**
 34 | 	 * Use plural table names
 35 | 	 *
 36 | 	 * @default false
 37 | 	 */
 38 | 	usePlural?: boolean;
 39 | 
 40 | 	/**
 41 | 	 * Whether to execute multiple operations in a transaction.
 42 | 	 *
 43 | 	 * If the database doesn't support transactions,
 44 | 	 * set this to `false` and operations will be executed sequentially.
 45 | 	 * @default false
 46 | 	 */
 47 | 	transaction?: boolean;
 48 | }
 49 | 
 50 | interface PrismaClient {}
 51 | 
 52 | type PrismaClientInternal = {
 53 | 	$transaction: (
 54 | 		callback: (db: PrismaClient) => Promise<any> | any,
 55 | 	) => Promise<any>;
 56 | } & {
 57 | 	[model: string]: {
 58 | 		create: (data: any) => Promise<any>;
 59 | 		findFirst: (data: any) => Promise<any>;
 60 | 		findMany: (data: any) => Promise<any>;
 61 | 		update: (data: any) => Promise<any>;
 62 | 		updateMany: (data: any) => Promise<any>;
 63 | 		delete: (data: any) => Promise<any>;
 64 | 		[key: string]: any;
 65 | 	};
 66 | };
 67 | 
 68 | export const prismaAdapter = (prisma: PrismaClient, config: PrismaConfig) => {
 69 | 	let lazyOptions: BetterAuthOptions | null = null;
 70 | 	const createCustomAdapter =
 71 | 		(prisma: PrismaClient): AdapterFactoryCustomizeAdapterCreator =>
 72 | 		({ getFieldName }) => {
 73 | 			const db = prisma as PrismaClientInternal;
 74 | 
 75 | 			const convertSelect = (select?: string[], model?: string) => {
 76 | 				if (!select || !model) return undefined;
 77 | 				return select.reduce((prev, cur) => {
 78 | 					return {
 79 | 						...prev,
 80 | 						[getFieldName({ model, field: cur })]: true,
 81 | 					};
 82 | 				}, {});
 83 | 			};
 84 | 			function operatorToPrismaOperator(operator: string) {
 85 | 				switch (operator) {
 86 | 					case "starts_with":
 87 | 						return "startsWith";
 88 | 					case "ends_with":
 89 | 						return "endsWith";
 90 | 					case "ne":
 91 | 						return "not";
 92 | 					case "not_in":
 93 | 						return "notIn";
 94 | 					default:
 95 | 						return operator;
 96 | 				}
 97 | 			}
 98 | 			const convertWhereClause = (model: string, where?: Where[]) => {
 99 | 				if (!where || !where.length) return {};
100 | 				if (where.length === 1) {
101 | 					const w = where[0]!;
102 | 					if (!w) {
103 | 						return;
104 | 					}
105 | 					return {
106 | 						[getFieldName({ model, field: w.field })]:
107 | 							w.operator === "eq" || !w.operator
108 | 								? w.value
109 | 								: {
110 | 										[operatorToPrismaOperator(w.operator)]: w.value,
111 | 									},
112 | 					};
113 | 				}
114 | 				const and = where.filter((w) => w.connector === "AND" || !w.connector);
115 | 				const or = where.filter((w) => w.connector === "OR");
116 | 				const andClause = and.map((w) => {
117 | 					return {
118 | 						[getFieldName({ model, field: w.field })]:
119 | 							w.operator === "eq" || !w.operator
120 | 								? w.value
121 | 								: {
122 | 										[operatorToPrismaOperator(w.operator)]: w.value,
123 | 									},
124 | 					};
125 | 				});
126 | 				const orClause = or.map((w) => {
127 | 					return {
128 | 						[getFieldName({ model, field: w.field })]:
129 | 							w.operator === "eq" || !w.operator
130 | 								? w.value
131 | 								: {
132 | 										[operatorToPrismaOperator(w.operator)]: w.value,
133 | 									},
134 | 					};
135 | 				});
136 | 
137 | 				return {
138 | 					...(andClause.length ? { AND: andClause } : {}),
139 | 					...(orClause.length ? { OR: orClause } : {}),
140 | 				};
141 | 			};
142 | 
143 | 			return {
144 | 				async create({ model, data: values, select }) {
145 | 					if (!db[model]) {
146 | 						throw new BetterAuthError(
147 | 							`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
148 | 						);
149 | 					}
150 | 					return await db[model]!.create({
151 | 						data: values,
152 | 						select: convertSelect(select, model),
153 | 					});
154 | 				},
155 | 				async findOne({ model, where, select }) {
156 | 					const whereClause = convertWhereClause(model, where);
157 | 					if (!db[model]) {
158 | 						throw new BetterAuthError(
159 | 							`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
160 | 						);
161 | 					}
162 | 					return await db[model]!.findFirst({
163 | 						where: whereClause,
164 | 						select: convertSelect(select, model),
165 | 					});
166 | 				},
167 | 				async findMany({ model, where, limit, offset, sortBy }) {
168 | 					const whereClause = convertWhereClause(model, where);
169 | 					if (!db[model]) {
170 | 						throw new BetterAuthError(
171 | 							`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
172 | 						);
173 | 					}
174 | 
175 | 					return (await db[model]!.findMany({
176 | 						where: whereClause,
177 | 						take: limit || 100,
178 | 						skip: offset || 0,
179 | 						...(sortBy?.field
180 | 							? {
181 | 									orderBy: {
182 | 										[getFieldName({ model, field: sortBy.field })]:
183 | 											sortBy.direction === "desc" ? "desc" : "asc",
184 | 									},
185 | 								}
186 | 							: {}),
187 | 					})) as any[];
188 | 				},
189 | 				async count({ model, where }) {
190 | 					const whereClause = convertWhereClause(model, where);
191 | 					if (!db[model]) {
192 | 						throw new BetterAuthError(
193 | 							`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
194 | 						);
195 | 					}
196 | 					return await db[model]!.count({
197 | 						where: whereClause,
198 | 					});
199 | 				},
200 | 				async update({ model, where, update }) {
201 | 					if (!db[model]) {
202 | 						throw new BetterAuthError(
203 | 							`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
204 | 						);
205 | 					}
206 | 					const whereClause = convertWhereClause(model, where);
207 | 					return await db[model]!.update({
208 | 						where: whereClause,
209 | 						data: update,
210 | 					});
211 | 				},
212 | 				async updateMany({ model, where, update }) {
213 | 					const whereClause = convertWhereClause(model, where);
214 | 					const result = await db[model]!.updateMany({
215 | 						where: whereClause,
216 | 						data: update,
217 | 					});
218 | 					return result ? (result.count as number) : 0;
219 | 				},
220 | 				async delete({ model, where }) {
221 | 					const whereClause = convertWhereClause(model, where);
222 | 					try {
223 | 						await db[model]!.delete({
224 | 							where: whereClause,
225 | 						});
226 | 					} catch (e: any) {
227 | 						// If the record doesn't exist, we don't want to throw an error
228 | 						if (e?.meta?.cause === "Record to delete does not exist.") return;
229 | 						// otherwise if it's an unknown error, we want to just log it for debugging.
230 | 						console.log(e);
231 | 					}
232 | 				},
233 | 				async deleteMany({ model, where }) {
234 | 					const whereClause = convertWhereClause(model, where);
235 | 					const result = await db[model]!.deleteMany({
236 | 						where: whereClause,
237 | 					});
238 | 					return result ? (result.count as number) : 0;
239 | 				},
240 | 				options: config,
241 | 			};
242 | 		};
243 | 
244 | 	let adapterOptions: AdapterFactoryOptions | null = null;
245 | 	adapterOptions = {
246 | 		config: {
247 | 			adapterId: "prisma",
248 | 			adapterName: "Prisma Adapter",
249 | 			usePlural: config.usePlural ?? false,
250 | 			debugLogs: config.debugLogs ?? false,
251 | 			transaction:
252 | 				(config.transaction ?? false)
253 | 					? (cb) =>
254 | 							(prisma as PrismaClientInternal).$transaction((tx) => {
255 | 								const adapter = createAdapterFactory({
256 | 									config: adapterOptions!.config,
257 | 									adapter: createCustomAdapter(tx),
258 | 								})(lazyOptions!);
259 | 								return cb(adapter);
260 | 							})
261 | 					: false,
262 | 		},
263 | 		adapter: createCustomAdapter(prisma),
264 | 	};
265 | 
266 | 	const adapter = createAdapterFactory(adapterOptions);
267 | 	return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
268 | 		lazyOptions = options;
269 | 		return adapter(options);
270 | 	};
271 | };
272 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
  2 | import { getTestInstance } from "../../test-utils/test-instance";
  3 | import type { RateLimit } from "../../types";
  4 | 
  5 | describe(
  6 | 	"rate-limiter",
  7 | 	{
  8 | 		timeout: 10000,
  9 | 	},
 10 | 	async () => {
 11 | 		const { client, testUser } = await getTestInstance({
 12 | 			rateLimit: {
 13 | 				enabled: true,
 14 | 				window: 10,
 15 | 				max: 20,
 16 | 			},
 17 | 		});
 18 | 
 19 | 		it("should return 429 after 3 request for sign-in", async () => {
 20 | 			for (let i = 0; i < 5; i++) {
 21 | 				const response = await client.signIn.email({
 22 | 					email: testUser.email,
 23 | 					password: testUser.password,
 24 | 				});
 25 | 				if (i >= 3) {
 26 | 					expect(response.error?.status).toBe(429);
 27 | 				} else {
 28 | 					expect(response.error).toBeNull();
 29 | 				}
 30 | 			}
 31 | 		});
 32 | 
 33 | 		it("should reset the limit after the window period", async () => {
 34 | 			vi.useFakeTimers();
 35 | 			vi.advanceTimersByTime(11000);
 36 | 			for (let i = 0; i < 5; i++) {
 37 | 				const res = await client.signIn.email({
 38 | 					email: testUser.email,
 39 | 					password: testUser.password,
 40 | 				});
 41 | 				if (i >= 3) {
 42 | 					expect(res.error?.status).toBe(429);
 43 | 				} else {
 44 | 					expect(res.error).toBeNull();
 45 | 				}
 46 | 			}
 47 | 		});
 48 | 
 49 | 		it("should respond the correct retry-after header", async () => {
 50 | 			vi.useFakeTimers();
 51 | 			vi.advanceTimersByTime(3000);
 52 | 			let retryAfter = "";
 53 | 			await client.signIn.email(
 54 | 				{
 55 | 					email: testUser.email,
 56 | 					password: testUser.password,
 57 | 				},
 58 | 				{
 59 | 					onError(context) {
 60 | 						retryAfter = context.response.headers.get("X-Retry-After") ?? "";
 61 | 					},
 62 | 				},
 63 | 			);
 64 | 			expect(retryAfter).toBe("7");
 65 | 		});
 66 | 
 67 | 		it("should rate limit based on the path", async () => {
 68 | 			const signInRes = await client.signIn.email({
 69 | 				email: testUser.email,
 70 | 				password: testUser.password,
 71 | 			});
 72 | 			expect(signInRes.error?.status).toBe(429);
 73 | 
 74 | 			const signUpRes = await client.signUp.email({
 75 | 				email: "[email protected]",
 76 | 				password: testUser.password,
 77 | 				name: "test",
 78 | 			});
 79 | 			expect(signUpRes.error).toBeNull();
 80 | 		});
 81 | 
 82 | 		it("non-special-rules limits", async () => {
 83 | 			for (let i = 0; i < 25; i++) {
 84 | 				const response = await client.getSession();
 85 | 				expect(response.error?.status).toBe(i >= 20 ? 429 : undefined);
 86 | 			}
 87 | 		});
 88 | 
 89 | 		it("query params should be ignored", async () => {
 90 | 			for (let i = 0; i < 25; i++) {
 91 | 				const response = await client.listSessions({
 92 | 					fetchOptions: {
 93 | 						query: {
 94 | 							"test-query": Math.random().toString(),
 95 | 						},
 96 | 					},
 97 | 				});
 98 | 
 99 | 				if (i >= 20) {
100 | 					expect(response.error?.status).toBe(429);
101 | 				} else {
102 | 					expect(response.error?.status).toBe(401);
103 | 				}
104 | 			}
105 | 		});
106 | 	},
107 | );
108 | 
109 | describe("custom rate limiting storage", async () => {
110 | 	let store = new Map<string, string>();
111 | 	const expirationMap = new Map<string, number>();
112 | 	const { client, testUser } = await getTestInstance({
113 | 		rateLimit: {
114 | 			enabled: true,
115 | 		},
116 | 		secondaryStorage: {
117 | 			set(key, value, ttl) {
118 | 				store.set(key, value);
119 | 				if (ttl) expirationMap.set(key, ttl);
120 | 			},
121 | 			get(key) {
122 | 				return store.get(key) || null;
123 | 			},
124 | 			delete(key) {
125 | 				store.delete(key);
126 | 				expirationMap.delete(key);
127 | 			},
128 | 		},
129 | 	});
130 | 
131 | 	it("should use custom storage", async () => {
132 | 		await client.getSession();
133 | 		expect(store.size).toBe(3);
134 | 		let lastRequest = Date.now();
135 | 		for (let i = 0; i < 4; i++) {
136 | 			const response = await client.signIn.email({
137 | 				email: testUser.email,
138 | 				password: testUser.password,
139 | 			});
140 | 			const rateLimitData: RateLimit = JSON.parse(
141 | 				store.get("127.0.0.1/sign-in/email") ?? "{}",
142 | 			);
143 | 			expect(rateLimitData.lastRequest).toBeGreaterThanOrEqual(lastRequest);
144 | 			lastRequest = rateLimitData.lastRequest;
145 | 			if (i >= 3) {
146 | 				expect(response.error?.status).toBe(429);
147 | 				expect(rateLimitData.count).toBe(3);
148 | 			} else {
149 | 				expect(response.error).toBeNull();
150 | 				expect(rateLimitData.count).toBe(i + 1);
151 | 			}
152 | 			const rateLimitExp = expirationMap.get("127.0.0.1/sign-in/email");
153 | 			expect(rateLimitExp).toBe(10);
154 | 		}
155 | 	});
156 | });
157 | 
158 | describe("should work with custom rules", async () => {
159 | 	const { client, testUser } = await getTestInstance({
160 | 		rateLimit: {
161 | 			enabled: true,
162 | 			storage: "database",
163 | 			customRules: {
164 | 				"/sign-in/*": {
165 | 					window: 10,
166 | 					max: 2,
167 | 				},
168 | 				"/sign-up/email": {
169 | 					window: 10,
170 | 					max: 3,
171 | 				},
172 | 				"/get-session": false,
173 | 			},
174 | 		},
175 | 	});
176 | 
177 | 	it("should use custom rules", async () => {
178 | 		for (let i = 0; i < 4; i++) {
179 | 			const response = await client.signIn.email({
180 | 				email: testUser.email,
181 | 				password: testUser.password,
182 | 			});
183 | 			if (i >= 2) {
184 | 				expect(response.error?.status).toBe(429);
185 | 			} else {
186 | 				expect(response.error).toBeNull();
187 | 			}
188 | 		}
189 | 
190 | 		for (let i = 0; i < 5; i++) {
191 | 			const response = await client.signUp.email({
192 | 				email: `${Math.random()}@test.com`,
193 | 				password: testUser.password,
194 | 				name: "test",
195 | 			});
196 | 			if (i >= 3) {
197 | 				expect(response.error?.status).toBe(429);
198 | 			} else {
199 | 				expect(response.error).toBeNull();
200 | 			}
201 | 		}
202 | 	});
203 | 
204 | 	it("should use default rules if custom rules are not defined", async () => {
205 | 		for (let i = 0; i < 5; i++) {
206 | 			const response = await client.getSession();
207 | 			if (i >= 20) {
208 | 				expect(response.error?.status).toBe(429);
209 | 			} else {
210 | 				expect(response.error).toBeNull();
211 | 			}
212 | 		}
213 | 	});
214 | 
215 | 	it("should not rate limit if custom rule is false", async () => {
216 | 		let i = 0;
217 | 		let response = null;
218 | 		for (; i < 110; i++) {
219 | 			response = await client.getSession().then((res) => res.error);
220 | 		}
221 | 		expect(response).toBeNull();
222 | 		expect(i).toBe(110);
223 | 	});
224 | });
225 | 
226 | describe("should work in development/test environment", () => {
227 | 	const LOCALHOST_IP = "127.0.0.1";
228 | 	const REQUEST_PATH = "/sign-in/email";
229 | 
230 | 	let originalNodeEnv: string | undefined;
231 | 	beforeEach(() => {
232 | 		originalNodeEnv = process.env.NODE_ENV;
233 | 	});
234 | 	afterEach(() => {
235 | 		process.env.NODE_ENV = originalNodeEnv;
236 | 		vi.unstubAllEnvs();
237 | 	});
238 | 
239 | 	it("should work in development environment", async () => {
240 | 		vi.stubEnv("NODE_ENV", "development");
241 | 
242 | 		const store = new Map<string, string>();
243 | 		const { client, testUser } = await getTestInstance({
244 | 			rateLimit: {
245 | 				enabled: true,
246 | 				window: 10,
247 | 				max: 3,
248 | 			},
249 | 			secondaryStorage: {
250 | 				set(key, value) {
251 | 					store.set(key, value);
252 | 				},
253 | 				get(key) {
254 | 					return store.get(key) || null;
255 | 				},
256 | 				delete(key) {
257 | 					store.delete(key);
258 | 				},
259 | 			},
260 | 		});
261 | 
262 | 		for (let i = 0; i < 4; i++) {
263 | 			const response = await client.signIn.email({
264 | 				email: testUser.email,
265 | 				password: testUser.password,
266 | 			});
267 | 
268 | 			if (i >= 3) {
269 | 				expect(response.error?.status).toBe(429);
270 | 			} else {
271 | 				expect(response.error).toBeNull();
272 | 			}
273 | 		}
274 | 
275 | 		const signInKeys = Array.from(store.keys()).filter((key) =>
276 | 			key.endsWith(REQUEST_PATH),
277 | 		);
278 | 
279 | 		expect(signInKeys.length).toBeGreaterThan(0);
280 | 		expect(signInKeys[0]).toBe(`${LOCALHOST_IP}${REQUEST_PATH}`);
281 | 	});
282 | 
283 | 	it("should work in test environment", async () => {
284 | 		vi.stubEnv("NODE_ENV", "test");
285 | 
286 | 		const store = new Map<string, string>();
287 | 		const { client, testUser } = await getTestInstance({
288 | 			rateLimit: {
289 | 				enabled: true,
290 | 				window: 10,
291 | 				max: 3,
292 | 			},
293 | 			secondaryStorage: {
294 | 				set(key, value) {
295 | 					store.set(key, value);
296 | 				},
297 | 				get(key) {
298 | 					return store.get(key) || null;
299 | 				},
300 | 				delete(key) {
301 | 					store.delete(key);
302 | 				},
303 | 			},
304 | 		});
305 | 
306 | 		for (let i = 0; i < 4; i++) {
307 | 			const response = await client.signIn.email({
308 | 				email: testUser.email,
309 | 				password: testUser.password,
310 | 			});
311 | 
312 | 			if (i >= 3) {
313 | 				expect(response.error?.status).toBe(429);
314 | 			} else {
315 | 				expect(response.error).toBeNull();
316 | 			}
317 | 		}
318 | 
319 | 		const signInKeys = Array.from(store.keys()).filter((key) =>
320 | 			key.endsWith(REQUEST_PATH),
321 | 		);
322 | 
323 | 		expect(signInKeys.length).toBeGreaterThan(0);
324 | 		expect(signInKeys[0]).toBe(`${LOCALHOST_IP}${REQUEST_PATH}`);
325 | 	});
326 | });
327 | 
```
Page 23/69FirstPrevNextLast