#
tokens: 49348/50000 13/1100 files (page 20/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 20 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

--------------------------------------------------------------------------------
/docs/app/api/og-release/route.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { ImageResponse } from "@vercel/og";
  2 | import { z } from "zod";
  3 | export const runtime = "edge";
  4 | 
  5 | const ogSchema = z.object({
  6 | 	heading: z.string(),
  7 | 	description: z.string().optional(),
  8 | 	date: z.string().optional(),
  9 | });
 10 | 
 11 | export async function GET(req: Request) {
 12 | 	try {
 13 | 		const geist = await fetch(
 14 | 			new URL("../../../assets/Geist.ttf", import.meta.url),
 15 | 		).then((res) => res.arrayBuffer());
 16 | 
 17 | 		const url = new URL(req.url);
 18 | 		const urlParamsValues = Object.fromEntries(url.searchParams);
 19 | 		const validParams = ogSchema.parse(urlParamsValues);
 20 | 
 21 | 		const { heading, description, date } = validParams;
 22 | 		const trueHeading =
 23 | 			heading.length > 140 ? `${heading.substring(0, 140)}...` : heading;
 24 | 
 25 | 		return new ImageResponse(
 26 | 			<div
 27 | 				tw="flex w-full h-full relative flex-col"
 28 | 				style={{
 29 | 					background:
 30 | 						"radial-gradient(circle 230px at 0% 0%, #000000, #000000)",
 31 | 					fontFamily: "Geist",
 32 | 					color: "white",
 33 | 				}}
 34 | 			>
 35 | 				<div
 36 | 					tw="flex w-full h-full relative"
 37 | 					style={{
 38 | 						borderRadius: "10px",
 39 | 						border: "1px solid rgba(32, 34, 34, 0.5)",
 40 | 					}}
 41 | 				>
 42 | 					<div
 43 | 						tw="absolute"
 44 | 						style={{
 45 | 							width: "350px",
 46 | 							height: "120px",
 47 | 							borderRadius: "100px",
 48 | 							background: "#c7c7c7",
 49 | 							opacity: 0.21,
 50 | 							filter: "blur(35px)",
 51 | 							transform: "rotate(50deg)",
 52 | 							top: "18%",
 53 | 							left: "0%",
 54 | 						}}
 55 | 					/>
 56 | 
 57 | 					<div
 58 | 						tw="flex flex-col w-full relative h-full p-8"
 59 | 						style={{
 60 | 							gap: "14px",
 61 | 							position: "relative",
 62 | 							zIndex: 999,
 63 | 						}}
 64 | 					>
 65 | 						<div
 66 | 							tw="absolute bg-repeat w-full h-full"
 67 | 							style={{
 68 | 								width: "100%",
 69 | 								height: "100%",
 70 | 								zIndex: 999,
 71 | 
 72 | 								background:
 73 | 									"url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnN2Z2pzPSJodHRwOi8vc3ZnanMuZGV2L3N2Z2pzIiB2aWV3Qm94PSIwIDAgODAwIDgwMCIgd2lkdGg9IjgwMCIgaGVpZ2h0PSI4MDAiPjxnIHN0cm9rZS13aWR0aD0iMy41IiBzdHJva2U9ImhzbGEoMCwgMCUsIDEwMCUsIDEuMDApIiBmaWxsPSJub25lIiBvcGFjaXR5PSIwLjUiPjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iNDAwIiB4PSIwIiB5PSIwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjMxNTc4OTQ3MzYiIGN4PSIwIiBjeT0iMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjQwMCIgeT0iMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iNDAwIiBjeT0iMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjgwMCIgeT0iMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iODAwIiBjeT0iMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjAiIHk9IjQwMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iMCIgY3k9IjQwMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjQwMCIgeT0iNDAwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjMxNTc4OTQ3MzYiIGN4PSI0MDAiIGN5PSI0MDAiIGZpbGw9ImhzbGEoMCwgMCUsIDEwMCUsIDEuMDApIiBzdHJva2U9Im5vbmUiPjwvY2lyY2xlPjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iNDAwIiB4PSI4MDAiIHk9IjQwMCIgb3BhY2l0eT0iMC4xNSI+PC9yZWN0PjxjaXJjbGUgcj0iMTAuODU1MjYzMTU3ODk0NzM2IiBjeD0iODAwIiBjeT0iNDAwIiBmaWxsPSJoc2xhKDAsIDAlLCAxMDAlLCAxLjAwKSIgc3Ryb2tlPSJub25lIj48L2NpcmNsZT48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeD0iMCIgeT0iODAwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjMxNTc4OTQ3MzYiIGN4PSIwIiBjeT0iODAwIiBmaWxsPSJoc2xhKDAsIDAlLCAxMDAlLCAxLjAwKSIgc3Ryb2tlPSJub25lIj48L2NpcmNsZT48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeD0iNDAwIiB5PSI4MDAiIG9wYWNpdHk9IjAuMTUiPjwvcmVjdD48Y2lyY2xlIHI9IjEwLjg1NTI2MzE1Nzg5NDczNiIgY3g9IjQwMCIgY3k9IjgwMCIgZmlsbD0iaHNsYSgwLCAwJSwgMTAwJSwgMS4wMCkiIHN0cm9rZT0ibm9uZSI+PC9jaXJjbGU+PHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIHg9IjgwMCIgeT0iODAwIiBvcGFjaXR5PSIwLjE1Ij48L3JlY3Q+PGNpcmNsZSByPSIxMC44NTUyNjM1NTc4OTQ3MzYiIGN4PSI4MDAiIGN5PSI4MDAiIGZpbGw9ImhzbGEoMCwgMCUsIDEwMCUsIDEuMDApIiBzdHJva2U9Im5vbmUiPjwvY2lyY2xlPjwvZz48L3N2Zz4=')",
 74 | 								backgroundSize: "25px 25px",
 75 | 								display: "flex",
 76 | 								alignItems: "flex-start",
 77 | 								justifyContent: "flex-start",
 78 | 								position: "relative",
 79 | 								flexDirection: "column",
 80 | 								textAlign: "left",
 81 | 								paddingLeft: "170px",
 82 | 								gap: "14px",
 83 | 							}}
 84 | 						/>
 85 | 						<div
 86 | 							tw="flex text-6xl absolute bottom-56 isolate font-bold"
 87 | 							style={{
 88 | 								paddingLeft: "170px",
 89 | 								paddingTop: "200px",
 90 | 								background: "linear-gradient(45deg, #000000 4%, #fff, #000)",
 91 | 								backgroundClip: "text",
 92 | 								color: "transparent",
 93 | 							}}
 94 | 						>
 95 | 							{trueHeading}
 96 | 						</div>
 97 | 
 98 | 						<div
 99 | 							tw="flex absolute bottom-44 z-[999] text-2xl"
100 | 							style={{
101 | 								paddingLeft: "170px",
102 | 								background:
103 | 									"linear-gradient(10deg, #d4d4d8, 04%, #fff, #d4d4d8)",
104 | 								backgroundClip: "text",
105 | 								opacity: 0.7,
106 | 								color: "transparent",
107 | 							}}
108 | 						>
109 | 							{description}
110 | 						</div>
111 | 
112 | 						<div
113 | 							tw="flex text-2xl absolute bottom-28 z-[999]"
114 | 							style={{
115 | 								paddingLeft: "170px",
116 | 								background:
117 | 									"linear-gradient(10deg, #d4d4d8, 04%, #fff, #d4d4d8)",
118 | 								backgroundClip: "text",
119 | 								opacity: 0.8,
120 | 								color: "transparent",
121 | 							}}
122 | 						>
123 | 							{date}
124 | 						</div>
125 | 					</div>
126 | 
127 | 					{/* Lines */}
128 | 					<div
129 | 						tw="absolute top-10% w-full h-px"
130 | 						style={{
131 | 							background: "linear-gradient(90deg, #888888 30%, #1d1f1f 70%)",
132 | 						}}
133 | 					/>
134 | 					<div
135 | 						tw="absolute bottom-10% w-full h-px"
136 | 						style={{
137 | 							background: "#2c2c2c",
138 | 						}}
139 | 					/>
140 | 					<div
141 | 						tw="absolute left-10% h-full w-px"
142 | 						style={{
143 | 							background: "linear-gradient(180deg, #747474 30%, #222424 70%)",
144 | 						}}
145 | 					/>
146 | 					<div
147 | 						tw="absolute right-10% h-full w-px"
148 | 						style={{
149 | 							background: "#2c2c2c",
150 | 						}}
151 | 					/>
152 | 				</div>
153 | 			</div>,
154 | 			{
155 | 				width: 1200,
156 | 				height: 630,
157 | 				fonts: [
158 | 					{
159 | 						name: "Geist",
160 | 						data: geist,
161 | 						weight: 400,
162 | 						style: "normal",
163 | 					},
164 | 				],
165 | 			},
166 | 		);
167 | 	} catch (err) {
168 | 		console.log({ err });
169 | 		return new Response("Failed to generate the OG image", { status: 500 });
170 | 	}
171 | }
172 | 
```

--------------------------------------------------------------------------------
/packages/core/src/social-providers/apple.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { betterFetch } from "@better-fetch/fetch";
  2 | import { APIError } from "better-call";
  3 | import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
  4 | import type { OAuthProvider, ProviderOptions } from "../oauth2";
  5 | import {
  6 | 	refreshAccessToken,
  7 | 	createAuthorizationURL,
  8 | 	validateAuthorizationCode,
  9 | } from "../oauth2";
 10 | export interface AppleProfile {
 11 | 	/**
 12 | 	 * The subject registered claim identifies the principal that’s the subject
 13 | 	 * of the identity token. Because this token is for your app, the value is
 14 | 	 * the unique identifier for the user.
 15 | 	 */
 16 | 	sub: string;
 17 | 	/**
 18 | 	 * A String value representing the user's email address.
 19 | 	 * The email address is either the user's real email address or the proxy
 20 | 	 * address, depending on their status private email relay service.
 21 | 	 */
 22 | 	email: string;
 23 | 	/**
 24 | 	 * A string or Boolean value that indicates whether the service verifies
 25 | 	 * the email. The value can either be a string ("true" or "false") or a
 26 | 	 * Boolean (true or false). The system may not verify email addresses for
 27 | 	 * Sign in with Apple at Work & School users, and this claim is "false" or
 28 | 	 * false for those users.
 29 | 	 */
 30 | 	email_verified: true | "true";
 31 | 	/**
 32 | 	 * A string or Boolean value that indicates whether the email that the user
 33 | 	 * shares is the proxy address. The value can either be a string ("true" or
 34 | 	 * "false") or a Boolean (true or false).
 35 | 	 */
 36 | 	is_private_email: boolean;
 37 | 	/**
 38 | 	 * An Integer value that indicates whether the user appears to be a real
 39 | 	 * person. Use the value of this claim to mitigate fraud. The possible
 40 | 	 * values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal). For
 41 | 	 * more information, see ASUserDetectionStatus. This claim is present only
 42 | 	 * in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14
 43 | 	 * and later. The claim isn’t present or supported for web-based apps.
 44 | 	 */
 45 | 	real_user_status: number;
 46 | 	/**
 47 | 	 * The user’s full name in the format provided during the authorization
 48 | 	 * process.
 49 | 	 */
 50 | 	name: string;
 51 | 	/**
 52 | 	 * The URL to the user's profile picture.
 53 | 	 */
 54 | 	picture: string;
 55 | 	user?: AppleNonConformUser;
 56 | }
 57 | 
 58 | /**
 59 |  * This is the shape of the `user` query parameter that Apple sends the first
 60 |  * time the user consents to the app.
 61 |  * @see https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server./
 62 |  */
 63 | export interface AppleNonConformUser {
 64 | 	name: {
 65 | 		firstName: string;
 66 | 		lastName: string;
 67 | 	};
 68 | 	email: string;
 69 | }
 70 | 
 71 | export interface AppleOptions extends ProviderOptions<AppleProfile> {
 72 | 	clientId: string;
 73 | 	appBundleIdentifier?: string;
 74 | 	audience?: string | string[];
 75 | }
 76 | 
 77 | export const apple = (options: AppleOptions) => {
 78 | 	const tokenEndpoint = "https://appleid.apple.com/auth/token";
 79 | 	return {
 80 | 		id: "apple",
 81 | 		name: "Apple",
 82 | 		async createAuthorizationURL({ state, scopes, redirectURI }) {
 83 | 			const _scope = options.disableDefaultScope ? [] : ["email", "name"];
 84 | 			options.scope && _scope.push(...options.scope);
 85 | 			scopes && _scope.push(...scopes);
 86 | 			const url = await createAuthorizationURL({
 87 | 				id: "apple",
 88 | 				options,
 89 | 				authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
 90 | 				scopes: _scope,
 91 | 				state,
 92 | 				redirectURI,
 93 | 				responseMode: "form_post",
 94 | 				responseType: "code id_token",
 95 | 			});
 96 | 			return url;
 97 | 		},
 98 | 		validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
 99 | 			return validateAuthorizationCode({
100 | 				code,
101 | 				codeVerifier,
102 | 				redirectURI,
103 | 				options,
104 | 				tokenEndpoint,
105 | 			});
106 | 		},
107 | 		async verifyIdToken(token, nonce) {
108 | 			if (options.disableIdTokenSignIn) {
109 | 				return false;
110 | 			}
111 | 			if (options.verifyIdToken) {
112 | 				return options.verifyIdToken(token, nonce);
113 | 			}
114 | 			const decodedHeader = decodeProtectedHeader(token);
115 | 			const { kid, alg: jwtAlg } = decodedHeader;
116 | 			if (!kid || !jwtAlg) return false;
117 | 			const publicKey = await getApplePublicKey(kid);
118 | 			const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
119 | 				algorithms: [jwtAlg],
120 | 				issuer: "https://appleid.apple.com",
121 | 				audience:
122 | 					options.audience && options.audience.length
123 | 						? options.audience
124 | 						: options.appBundleIdentifier
125 | 							? options.appBundleIdentifier
126 | 							: options.clientId,
127 | 				maxTokenAge: "1h",
128 | 			});
129 | 			["email_verified", "is_private_email"].forEach((field) => {
130 | 				if (jwtClaims[field] !== undefined) {
131 | 					jwtClaims[field] = Boolean(jwtClaims[field]);
132 | 				}
133 | 			});
134 | 			if (nonce && jwtClaims.nonce !== nonce) {
135 | 				return false;
136 | 			}
137 | 			return !!jwtClaims;
138 | 		},
139 | 		refreshAccessToken: options.refreshAccessToken
140 | 			? options.refreshAccessToken
141 | 			: async (refreshToken) => {
142 | 					return refreshAccessToken({
143 | 						refreshToken,
144 | 						options: {
145 | 							clientId: options.clientId,
146 | 							clientKey: options.clientKey,
147 | 							clientSecret: options.clientSecret,
148 | 						},
149 | 						tokenEndpoint: "https://appleid.apple.com/auth/token",
150 | 					});
151 | 				},
152 | 		async getUserInfo(token) {
153 | 			if (options.getUserInfo) {
154 | 				return options.getUserInfo(token);
155 | 			}
156 | 			if (!token.idToken) {
157 | 				return null;
158 | 			}
159 | 			const profile = decodeJwt<AppleProfile>(token.idToken);
160 | 			if (!profile) {
161 | 				return null;
162 | 			}
163 | 			const name = token.user
164 | 				? `${token.user.name?.firstName} ${token.user.name?.lastName}`
165 | 				: profile.name || profile.email;
166 | 			const emailVerified =
167 | 				typeof profile.email_verified === "boolean"
168 | 					? profile.email_verified
169 | 					: profile.email_verified === "true";
170 | 			const enrichedProfile = {
171 | 				...profile,
172 | 				name,
173 | 			};
174 | 			const userMap = await options.mapProfileToUser?.(enrichedProfile);
175 | 			return {
176 | 				user: {
177 | 					id: profile.sub,
178 | 					name: enrichedProfile.name,
179 | 					emailVerified: emailVerified,
180 | 					email: profile.email,
181 | 					...userMap,
182 | 				},
183 | 				data: enrichedProfile,
184 | 			};
185 | 		},
186 | 		options,
187 | 	} satisfies OAuthProvider<AppleProfile>;
188 | };
189 | 
190 | export const getApplePublicKey = async (kid: string) => {
191 | 	const APPLE_BASE_URL = "https://appleid.apple.com";
192 | 	const JWKS_APPLE_URI = "/auth/keys";
193 | 	const { data } = await betterFetch<{
194 | 		keys: Array<{
195 | 			kid: string;
196 | 			alg: string;
197 | 			kty: string;
198 | 			use: string;
199 | 			n: string;
200 | 			e: string;
201 | 		}>;
202 | 	}>(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);
203 | 	if (!data?.keys) {
204 | 		throw new APIError("BAD_REQUEST", {
205 | 			message: "Keys not found",
206 | 		});
207 | 	}
208 | 	const jwk = data.keys.find((key) => key.kid === kid);
209 | 	if (!jwk) {
210 | 		throw new Error(`JWK with kid ${kid} not found`);
211 | 	}
212 | 	return await importJWK(jwk, jwk.alg);
213 | };
214 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/test-utils/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterAll } from "vitest";
  2 | import { betterAuth } from "../auth";
  3 | import { createAuthClient } from "../client/vanilla";
  4 | import type { BetterAuthOptions, Session, User } from "../types";
  5 | import { getMigrations } from "../db/get-migration";
  6 | import { parseSetCookieHeader, setCookieToHeader } from "../cookies";
  7 | import type { SuccessContext } from "@better-fetch/fetch";
  8 | import { getAdapter } from "../db/utils";
  9 | import { getBaseURL } from "../utils/url";
 10 | import { Kysely, MysqlDialect, PostgresDialect, sql } from "kysely";
 11 | import { Pool } from "pg";
 12 | import { MongoClient } from "mongodb";
 13 | import { mongodbAdapter } from "../adapters/mongodb-adapter";
 14 | import { createPool } from "mysql2/promise";
 15 | import { bearer } from "../plugins";
 16 | import type { BetterAuthClientOptions } from "@better-auth/core";
 17 | 
 18 | export async function getTestInstanceMemory<
 19 | 	O extends Partial<BetterAuthOptions>,
 20 | 	C extends BetterAuthClientOptions,
 21 | >(
 22 | 	options?: O,
 23 | 	config?: {
 24 | 		clientOptions?: C;
 25 | 		port?: number;
 26 | 		disableTestUser?: boolean;
 27 | 		testUser?: Partial<User>;
 28 | 		testWith?: "sqlite" | "postgres" | "mongodb" | "mysql" | "memory";
 29 | 	},
 30 | ) {
 31 | 	const testWith = config?.testWith || "memory";
 32 | 	const postgres = new Kysely({
 33 | 		dialect: new PostgresDialect({
 34 | 			pool: new Pool({
 35 | 				connectionString: "postgres://user:password@localhost:5432/better_auth",
 36 | 			}),
 37 | 		}),
 38 | 	});
 39 | 
 40 | 	const mysql = new Kysely({
 41 | 		dialect: new MysqlDialect(
 42 | 			createPool("mysql://user:password@localhost:3306/better_auth"),
 43 | 		),
 44 | 	});
 45 | 
 46 | 	async function mongodbClient() {
 47 | 		const dbClient = async (connectionString: string, dbName: string) => {
 48 | 			const client = new MongoClient(connectionString);
 49 | 			await client.connect();
 50 | 			const db = client.db(dbName);
 51 | 			return db;
 52 | 		};
 53 | 		const db = await dbClient("mongodb://127.0.0.1:27017", "better-auth");
 54 | 		return db;
 55 | 	}
 56 | 
 57 | 	const opts = {
 58 | 		socialProviders: {
 59 | 			github: {
 60 | 				clientId: "test",
 61 | 				clientSecret: "test",
 62 | 			},
 63 | 			google: {
 64 | 				clientId: "test",
 65 | 				clientSecret: "test",
 66 | 			},
 67 | 		},
 68 | 		secret: "better-auth.secret",
 69 | 		database:
 70 | 			testWith === "postgres"
 71 | 				? { db: postgres, type: "postgres" }
 72 | 				: testWith === "mongodb"
 73 | 					? mongodbAdapter(await mongodbClient())
 74 | 					: testWith === "mysql"
 75 | 						? { db: mysql, type: "mysql" }
 76 | 						: undefined,
 77 | 		emailAndPassword: {
 78 | 			enabled: true,
 79 | 		},
 80 | 		rateLimit: {
 81 | 			enabled: false,
 82 | 		},
 83 | 		advanced: {
 84 | 			cookies: {},
 85 | 		},
 86 | 	} satisfies BetterAuthOptions;
 87 | 
 88 | 	const auth = betterAuth({
 89 | 		baseURL: "http://localhost:" + (config?.port || 3000),
 90 | 		...opts,
 91 | 		...options,
 92 | 		advanced: {
 93 | 			disableCSRFCheck: true,
 94 | 			...options?.advanced,
 95 | 		},
 96 | 		plugins: [bearer(), ...(options?.plugins || [])],
 97 | 	} as unknown as O extends undefined ? typeof opts : O & typeof opts);
 98 | 
 99 | 	const testUser = {
100 | 		email: "[email protected]",
101 | 		password: "test123456",
102 | 		name: "test user",
103 | 		...config?.testUser,
104 | 	};
105 | 	async function createTestUser() {
106 | 		if (config?.disableTestUser) {
107 | 			return;
108 | 		}
109 | 		//@ts-expect-error
110 | 		const res = await auth.api.signUpEmail({
111 | 			body: testUser,
112 | 		});
113 | 	}
114 | 
115 | 	if (testWith !== "mongodb" && testWith !== "memory") {
116 | 		const { runMigrations } = await getMigrations({
117 | 			...auth.options,
118 | 			database: opts.database,
119 | 		});
120 | 		await runMigrations();
121 | 	}
122 | 
123 | 	await createTestUser();
124 | 
125 | 	afterAll(async () => {
126 | 		if (testWith === "mongodb") {
127 | 			const db = await mongodbClient();
128 | 			await db.dropDatabase();
129 | 			return;
130 | 		}
131 | 		if (testWith === "postgres") {
132 | 			await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute(
133 | 				postgres,
134 | 			);
135 | 			await postgres.destroy();
136 | 			return;
137 | 		}
138 | 
139 | 		if (testWith === "mysql") {
140 | 			await sql`SET FOREIGN_KEY_CHECKS = 0;`.execute(mysql);
141 | 			const tables = await mysql.introspection.getTables();
142 | 			for (const table of tables) {
143 | 				// @ts-expect-error
144 | 				await mysql.deleteFrom(table.name).execute();
145 | 			}
146 | 			await sql`SET FOREIGN_KEY_CHECKS = 1;`.execute(mysql);
147 | 			return;
148 | 		}
149 | 	});
150 | 
151 | 	async function signInWithTestUser() {
152 | 		if (config?.disableTestUser) {
153 | 			throw new Error("Test user is disabled");
154 | 		}
155 | 		let headers = new Headers();
156 | 		const setCookie = (name: string, value: string) => {
157 | 			const current = headers.get("cookie");
158 | 			headers.set("cookie", `${current || ""}; ${name}=${value}`);
159 | 		};
160 | 		//@ts-expect-error
161 | 		const { data, error } = await client.signIn.email({
162 | 			email: testUser.email,
163 | 			password: testUser.password,
164 | 			fetchOptions: {
165 | 				//@ts-expect-error
166 | 				onSuccess(context) {
167 | 					const header = context.response.headers.get("set-cookie");
168 | 					const cookies = parseSetCookieHeader(header || "");
169 | 					const signedCookie = cookies.get("better-auth.session_token")?.value;
170 | 					headers.set("cookie", `better-auth.session_token=${signedCookie}`);
171 | 				},
172 | 			},
173 | 		});
174 | 		return {
175 | 			session: data.session as Session,
176 | 			user: data.user as User,
177 | 			headers,
178 | 			setCookie,
179 | 		};
180 | 	}
181 | 	async function signInWithUser(email: string, password: string) {
182 | 		let headers = new Headers();
183 | 		//@ts-expect-error
184 | 		const { data } = await client.signIn.email({
185 | 			email,
186 | 			password,
187 | 			fetchOptions: {
188 | 				//@ts-expect-error
189 | 				onSuccess(context) {
190 | 					const header = context.response.headers.get("set-cookie");
191 | 					const cookies = parseSetCookieHeader(header || "");
192 | 					const signedCookie = cookies.get("better-auth.session_token")?.value;
193 | 					headers.set("cookie", `better-auth.session_token=${signedCookie}`);
194 | 				},
195 | 			},
196 | 		});
197 | 		return {
198 | 			res: data as {
199 | 				user: User;
200 | 				session: Session;
201 | 			},
202 | 			headers,
203 | 		};
204 | 	}
205 | 
206 | 	const customFetchImpl = async (
207 | 		url: string | URL | Request,
208 | 		init?: RequestInit,
209 | 	) => {
210 | 		return auth.handler(new Request(url, init));
211 | 	};
212 | 
213 | 	function sessionSetter(headers: Headers) {
214 | 		return (context: SuccessContext) => {
215 | 			const header = context.response.headers.get("set-cookie");
216 | 			if (header) {
217 | 				const cookies = parseSetCookieHeader(header || "");
218 | 				const signedCookie = cookies.get("better-auth.session_token")?.value;
219 | 				headers.set("cookie", `better-auth.session_token=${signedCookie}`);
220 | 			}
221 | 		};
222 | 	}
223 | 
224 | 	const client = createAuthClient({
225 | 		...(config?.clientOptions as C extends undefined ? {} : C),
226 | 		baseURL: getBaseURL(
227 | 			options?.baseURL || "http://localhost:" + (config?.port || 3000),
228 | 			options?.basePath || "/api/auth",
229 | 		),
230 | 		fetchOptions: {
231 | 			customFetchImpl,
232 | 		},
233 | 	});
234 | 	return {
235 | 		auth,
236 | 		client,
237 | 		testUser,
238 | 		signInWithTestUser,
239 | 		signInWithUser,
240 | 		cookieSetter: setCookieToHeader,
241 | 		customFetchImpl,
242 | 		sessionSetter,
243 | 		db: await getAdapter(auth.options),
244 | 	};
245 | }
246 | 
```

--------------------------------------------------------------------------------
/docs/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import * as React from "react";
  4 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
  5 | import { cva } from "class-variance-authority";
  6 | import { ChevronDownIcon } from "lucide-react";
  7 | 
  8 | import { cn } from "@/lib/utils";
  9 | 
 10 | function NavigationMenu({
 11 | 	className,
 12 | 	children,
 13 | 	viewport = true,
 14 | 	...props
 15 | }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
 16 | 	viewport?: boolean;
 17 | }) {
 18 | 	return (
 19 | 		<NavigationMenuPrimitive.Root
 20 | 			data-slot="navigation-menu"
 21 | 			data-viewport={viewport}
 22 | 			className={cn(
 23 | 				"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
 24 | 				className,
 25 | 			)}
 26 | 			{...props}
 27 | 		>
 28 | 			{children}
 29 | 			{viewport && <NavigationMenuViewport />}
 30 | 		</NavigationMenuPrimitive.Root>
 31 | 	);
 32 | }
 33 | 
 34 | function NavigationMenuList({
 35 | 	className,
 36 | 	...props
 37 | }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
 38 | 	return (
 39 | 		<NavigationMenuPrimitive.List
 40 | 			data-slot="navigation-menu-list"
 41 | 			className={cn(
 42 | 				"group flex flex-1 list-none items-center justify-center gap-1",
 43 | 				className,
 44 | 			)}
 45 | 			{...props}
 46 | 		/>
 47 | 	);
 48 | }
 49 | 
 50 | function NavigationMenuItem({
 51 | 	className,
 52 | 	...props
 53 | }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
 54 | 	return (
 55 | 		<NavigationMenuPrimitive.Item
 56 | 			data-slot="navigation-menu-item"
 57 | 			className={cn("relative", className)}
 58 | 			{...props}
 59 | 		/>
 60 | 	);
 61 | }
 62 | 
 63 | const navigationMenuTriggerStyle = cva(
 64 | 	"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1",
 65 | );
 66 | 
 67 | function NavigationMenuTrigger({
 68 | 	className,
 69 | 	children,
 70 | 	...props
 71 | }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
 72 | 	return (
 73 | 		<NavigationMenuPrimitive.Trigger
 74 | 			data-slot="navigation-menu-trigger"
 75 | 			className={cn(navigationMenuTriggerStyle(), "group", className)}
 76 | 			{...props}
 77 | 		>
 78 | 			{children}{" "}
 79 | 			<ChevronDownIcon
 80 | 				className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
 81 | 				aria-hidden="true"
 82 | 			/>
 83 | 		</NavigationMenuPrimitive.Trigger>
 84 | 	);
 85 | }
 86 | 
 87 | function NavigationMenuContent({
 88 | 	className,
 89 | 	...props
 90 | }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
 91 | 	return (
 92 | 		<NavigationMenuPrimitive.Content
 93 | 			data-slot="navigation-menu-content"
 94 | 			className={cn(
 95 | 				"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
 96 | 				"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
 97 | 				className,
 98 | 			)}
 99 | 			{...props}
100 | 		/>
101 | 	);
102 | }
103 | 
104 | function NavigationMenuViewport({
105 | 	className,
106 | 	...props
107 | }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
108 | 	return (
109 | 		<div
110 | 			className={cn(
111 | 				"absolute top-full left-0 isolate z-50 flex justify-center",
112 | 			)}
113 | 		>
114 | 			<NavigationMenuPrimitive.Viewport
115 | 				data-slot="navigation-menu-viewport"
116 | 				className={cn(
117 | 					"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
118 | 					className,
119 | 				)}
120 | 				{...props}
121 | 			/>
122 | 		</div>
123 | 	);
124 | }
125 | 
126 | function NavigationMenuLink({
127 | 	className,
128 | 	...props
129 | }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
130 | 	return (
131 | 		<NavigationMenuPrimitive.Link
132 | 			data-slot="navigation-menu-link"
133 | 			className={cn(
134 | 				"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
135 | 				className,
136 | 			)}
137 | 			{...props}
138 | 		/>
139 | 	);
140 | }
141 | 
142 | function NavigationMenuIndicator({
143 | 	className,
144 | 	...props
145 | }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
146 | 	return (
147 | 		<NavigationMenuPrimitive.Indicator
148 | 			data-slot="navigation-menu-indicator"
149 | 			className={cn(
150 | 				"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
151 | 				className,
152 | 			)}
153 | 			{...props}
154 | 		>
155 | 			<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
156 | 		</NavigationMenuPrimitive.Indicator>
157 | 	);
158 | }
159 | 
160 | export {
161 | 	NavigationMenu,
162 | 	NavigationMenuList,
163 | 	NavigationMenuItem,
164 | 	NavigationMenuContent,
165 | 	NavigationMenuTrigger,
166 | 	NavigationMenuLink,
167 | 	NavigationMenuIndicator,
168 | 	NavigationMenuViewport,
169 | 	navigationMenuTriggerStyle,
170 | };
171 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/concepts/email.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Email
  3 | description: Learn how to use email with Better Auth.
  4 | ---
  5 | 
  6 | Email is a key part of Better Auth, required for all users regardless of their authentication method. Better Auth provides email and password authentication out of the box, and a lot of utilities to help you manage email verification, password reset, and more.
  7 | 
  8 | 
  9 | ## Email Verification
 10 | 
 11 | Email verification is a security feature that ensures users provide a valid email address. It helps prevent spam and abuse by confirming that the email address belongs to the user. In this guide, you'll get a walk through of how to implement token based email verification in your app.
 12 | To use otp based email verification, check out the [OTP Verification](/docs/plugins/email-otp) guide.
 13 | 
 14 | ### Adding Email Verification to Your App
 15 | 
 16 | To enable email verification, you need to pass a function that sends a verification email with a link.
 17 | 
 18 | - **sendVerificationEmail**: This function is triggered when email verification starts. It accepts a data object with the following properties:
 19 |   - `user`: The user object containing the email address.
 20 |   - `url`: The verification URL the user must click to verify their email.
 21 |   - `token`: The verification token used to complete the email verification to be used when implementing a custom verification URL.
 22 | 
 23 | and a `request` object as the second parameter.
 24 | 
 25 | ```ts title="auth.ts"
 26 | import { betterAuth } from 'better-auth';
 27 | import { sendEmail } from './email'; // your email sending function
 28 | 
 29 | export const auth = betterAuth({
 30 |     emailVerification: {
 31 |         sendVerificationEmail: async ({ user, url, token }, request) => {
 32 |             await sendEmail({
 33 |                 to: user.email,
 34 |                 subject: 'Verify your email address',
 35 |                 text: `Click the link to verify your email: ${url}`
 36 |             })
 37 |         }
 38 |     }
 39 | })
 40 | ```
 41 | 
 42 | ### Triggering Email Verification
 43 | 
 44 | You can initiate email verification in several ways:
 45 | 
 46 | #### 1. During Sign-up
 47 | 
 48 | To automatically send a verification email at signup, set `emailVerification.sendOnSignUp` to `true`. 
 49 | 
 50 | ```ts title="auth.ts"
 51 | import { betterAuth } from 'better-auth';
 52 | 
 53 | export const auth = betterAuth({
 54 |     emailVerification: {
 55 |         sendOnSignUp: true
 56 |     }
 57 | })
 58 | ```
 59 | 
 60 | This sends a verification email when a user signs up. For social logins, email verification status is read from the SSO.
 61 | 
 62 | <Callout>
 63 |     With `sendOnSignUp` enabled, when the user logs in with an SSO that does not claim the email as verified, Better Auth will dispatch a verification email, but the verification is not required to login even when `requireEmailVerification` is enabled.
 64 | </Callout>
 65 | 
 66 | #### 2. Require Email Verification
 67 | 
 68 | If you enable require email verification, users must verify their email before they can log in. And every time a user tries to sign in, `sendVerificationEmail` is called.
 69 | 
 70 | <Callout>
 71 |     This only works if you have `sendVerificationEmail` implemented and if the user is trying to sign in with email and password.
 72 | </Callout>
 73 | 
 74 | ```ts title="auth.ts"
 75 | export const auth = betterAuth({
 76 |     emailAndPassword: {
 77 |         requireEmailVerification: true
 78 |     }
 79 | })
 80 | ```
 81 | 
 82 | if a user tries to sign in without verifying their email, you can handle the error and show a message to the user.
 83 | 
 84 | ```ts title="auth-client.ts"
 85 | await authClient.signIn.email({
 86 |     email: "[email protected]",
 87 |     password: "password"
 88 | }, {
 89 |     onError: (ctx) => {
 90 |         // Handle the error
 91 |         if(ctx.error.status === 403) {
 92 |             alert("Please verify your email address")
 93 |         }
 94 |         //you can also show the original error message
 95 |         alert(ctx.error.message)
 96 |     }
 97 | })
 98 | ```
 99 | 
100 | #### 3. Manually
101 | 
102 | You can also manually trigger email verification by calling `sendVerificationEmail`.
103 | 
104 | ```ts
105 | await authClient.sendVerificationEmail({
106 |     email: "[email protected]",
107 |     callbackURL: "/" // The redirect URL after verification
108 | })
109 | ```
110 | 
111 | ### Verifying the Email
112 | 
113 | If the user clicks the provided verification URL, their email is automatically verified, and they are redirected to the `callbackURL`.
114 | 
115 | For manual verification, you can send the user a custom link with the `token` and call the `verifyEmail` function.
116 | 
117 | ```ts
118 | await authClient.verifyEmail({
119 |     query: {
120 |         token: "" // Pass the token here
121 |     }
122 | })
123 | ```
124 | 
125 | ### Auto Sign In After Verification
126 | 
127 | To sign in the user automatically after they successfully verify their email, set the `autoSignInAfterVerification` option to `true`:
128 | 
129 | ```ts
130 | const auth = betterAuth({
131 |     //...your other options
132 |     emailVerification: {
133 |         autoSignInAfterVerification: true
134 |     }
135 | })
136 | ```
137 | 
138 | ### Callback after successful email verification
139 | 
140 | You can run custom code immediately after a user verifies their email using the `afterEmailVerification` callback. This is useful for any side-effects you want to trigger, like granting access to special features or logging the event.
141 | 
142 | The `afterEmailVerification` function runs automatically when a user's email is confirmed, receiving the `user` object and `request` details so you can perform actions for that specific user.
143 | 
144 | Here's how you can set it up:
145 | 
146 | ```ts title="auth.ts"
147 | import { betterAuth } from 'better-auth';
148 | 
149 | export const auth = betterAuth({
150 |     emailVerification: {
151 |         async afterEmailVerification(user, request) {
152 |             // Your custom logic here, e.g., grant access to premium features
153 |             console.log(`${user.email} has been successfully verified!`);
154 |         }
155 |     }
156 | })
157 | ```
158 | 
159 | ## Password Reset Email
160 | 
161 | Password reset allows users to reset their password if they forget it. Better Auth provides a simple way to implement password reset functionality.
162 | 
163 | You can enable password reset by passing a function that sends a password reset email with a link.
164 | 
165 | ```ts title="auth.ts"
166 | import { betterAuth } from 'better-auth';
167 | import { sendEmail } from './email'; // your email sending function
168 | 
169 | export const auth = betterAuth({
170 |     emailAndPassword: {
171 |         enabled: true,
172 |         sendResetPassword: async ({ user, url, token }, request) => {
173 |             await sendEmail({
174 |                 to: user.email,
175 |                 subject: 'Reset your password',
176 |                 text: `Click the link to reset your password: ${url}`
177 |             })
178 |         }
179 |     }
180 | })
181 | ```
182 | 
183 | Check out the [Email and Password](/docs/authentication/email-password#forget-password) guide for more details on how to implement password reset in your app.
184 | Also you can check out the [Otp verification](/docs/plugins/email-otp#reset-password) guide for how to implement password reset with OTP in your app.
185 | 
```

--------------------------------------------------------------------------------
/docs/components/docs/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import type { TableOfContents } from "fumadocs-core/server";
  2 | import {
  3 | 	type AnchorHTMLAttributes,
  4 | 	forwardRef,
  5 | 	type HTMLAttributes,
  6 | 	type ReactNode,
  7 | } from "react";
  8 | import { type AnchorProviderProps, AnchorProvider } from "fumadocs-core/toc";
  9 | import { replaceOrDefault } from "./shared";
 10 | import { cn } from "../../lib/utils";
 11 | import {
 12 | 	Footer,
 13 | 	type FooterProps,
 14 | 	LastUpdate,
 15 | 	TocPopoverHeader,
 16 | 	type BreadcrumbProps,
 17 | 	PageBody,
 18 | 	PageArticle,
 19 | } from "./page.client";
 20 | import {
 21 | 	Toc,
 22 | 	TOCItems,
 23 | 	TocPopoverTrigger,
 24 | 	TocPopoverContent,
 25 | 	type TOCProps,
 26 | 	TOCScrollArea,
 27 | } from "./layout/toc";
 28 | import { buttonVariants } from "./ui/button";
 29 | import { Edit, Text } from "lucide-react";
 30 | import { I18nLabel } from "fumadocs-ui/provider";
 31 | 
 32 | type TableOfContentOptions = Omit<TOCProps, "items" | "children"> &
 33 | 	Pick<AnchorProviderProps, "single"> & {
 34 | 		enabled: boolean;
 35 | 		component: ReactNode;
 36 | 	};
 37 | 
 38 | type TableOfContentPopoverOptions = Omit<TableOfContentOptions, "single">;
 39 | 
 40 | interface EditOnGitHubOptions
 41 | 	extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "children"> {
 42 | 	owner: string;
 43 | 	repo: string;
 44 | 
 45 | 	/**
 46 | 	 * SHA or ref (branch or tag) name.
 47 | 	 *
 48 | 	 * @defaultValue main
 49 | 	 */
 50 | 	sha?: string;
 51 | 
 52 | 	/**
 53 | 	 * File path in the repo
 54 | 	 */
 55 | 	path: string;
 56 | }
 57 | 
 58 | interface BreadcrumbOptions extends BreadcrumbProps {
 59 | 	enabled: boolean;
 60 | 	component: ReactNode;
 61 | 
 62 | 	/**
 63 | 	 * Show the full path to the current page
 64 | 	 *
 65 | 	 * @defaultValue false
 66 | 	 * @deprecated use `includePage` instead
 67 | 	 */
 68 | 	full?: boolean;
 69 | }
 70 | 
 71 | interface FooterOptions extends FooterProps {
 72 | 	enabled: boolean;
 73 | 	component: ReactNode;
 74 | }
 75 | 
 76 | export interface DocsPageProps {
 77 | 	toc?: TableOfContents;
 78 | 
 79 | 	/**
 80 | 	 * Extend the page to fill all available space
 81 | 	 *
 82 | 	 * @defaultValue false
 83 | 	 */
 84 | 	full?: boolean;
 85 | 
 86 | 	tableOfContent?: Partial<TableOfContentOptions>;
 87 | 	tableOfContentPopover?: Partial<TableOfContentPopoverOptions>;
 88 | 
 89 | 	/**
 90 | 	 * Replace or disable breadcrumb
 91 | 	 */
 92 | 	breadcrumb?: Partial<BreadcrumbOptions>;
 93 | 
 94 | 	/**
 95 | 	 * Footer navigation, you can disable it by passing `false`
 96 | 	 */
 97 | 	footer?: Partial<FooterOptions>;
 98 | 
 99 | 	editOnGithub?: EditOnGitHubOptions;
100 | 	lastUpdate?: Date | string | number;
101 | 
102 | 	container?: HTMLAttributes<HTMLDivElement>;
103 | 	article?: HTMLAttributes<HTMLElement>;
104 | 	children: ReactNode;
105 | }
106 | 
107 | export function DocsPage({
108 | 	toc = [],
109 | 	full = false,
110 | 	tableOfContentPopover: {
111 | 		enabled: tocPopoverEnabled,
112 | 		component: tocPopoverReplace,
113 | 		...tocPopoverOptions
114 | 	} = {},
115 | 	tableOfContent: {
116 | 		enabled: tocEnabled,
117 | 		component: tocReplace,
118 | 		...tocOptions
119 | 	} = {},
120 | 	...props
121 | }: DocsPageProps) {
122 | 	const isTocRequired =
123 | 		toc.length > 0 ||
124 | 		tocOptions.footer !== undefined ||
125 | 		tocOptions.header !== undefined;
126 | 
127 | 	// disable TOC on full mode, you can still enable it with `enabled` option.
128 | 	tocEnabled ??= !full && isTocRequired;
129 | 
130 | 	tocPopoverEnabled ??=
131 | 		toc.length > 0 ||
132 | 		tocPopoverOptions.header !== undefined ||
133 | 		tocPopoverOptions.footer !== undefined;
134 | 
135 | 	return (
136 | 		<AnchorProvider toc={toc} single={tocOptions.single}>
137 | 			<PageBody
138 | 				{...props.container}
139 | 				className={cn(props.container?.className)}
140 | 				style={
141 | 					{
142 | 						"--fd-tocnav-height": !tocPopoverEnabled ? "0px" : undefined,
143 | 						...props.container?.style,
144 | 					} as object
145 | 				}
146 | 			>
147 | 				{replaceOrDefault(
148 | 					{ enabled: tocPopoverEnabled, component: tocPopoverReplace },
149 | 					<TocPopoverHeader className="h-10">
150 | 						<TocPopoverTrigger className="w-full" items={toc} />
151 | 						<TocPopoverContent>
152 | 							{tocPopoverOptions.header}
153 | 							<TOCScrollArea isMenu>
154 | 								<TOCItems items={toc} />
155 | 							</TOCScrollArea>
156 | 							{tocPopoverOptions.footer}
157 | 						</TocPopoverContent>
158 | 					</TocPopoverHeader>,
159 | 					{
160 | 						items: toc,
161 | 						...tocPopoverOptions,
162 | 					},
163 | 				)}
164 | 				<PageArticle
165 | 					{...props.article}
166 | 					className={cn(
167 | 						full || !tocEnabled ? "max-w-[1120px]" : "max-w-[860px]",
168 | 						props.article?.className,
169 | 					)}
170 | 				>
171 | 					{props.children}
172 | 					<div role="none" className="flex-1" />
173 | 					<div className="flex flex-row flex-wrap items-center justify-between gap-4 empty:hidden">
174 | 						{props.editOnGithub ? (
175 | 							<EditOnGitHub {...props.editOnGithub} />
176 | 						) : null}
177 | 						{props.lastUpdate ? (
178 | 							<LastUpdate date={new Date(props.lastUpdate)} />
179 | 						) : null}
180 | 					</div>
181 | 					{replaceOrDefault(
182 | 						props.footer,
183 | 						<Footer items={props.footer?.items} />,
184 | 					)}
185 | 				</PageArticle>
186 | 			</PageBody>
187 | 			{replaceOrDefault(
188 | 				{ enabled: tocEnabled, component: tocReplace },
189 | 				<Toc>
190 | 					{tocOptions.header}
191 | 					<h3 className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground">
192 | 						<Text className="size-4" />
193 | 						<I18nLabel label="toc" />
194 | 					</h3>
195 | 					<TOCScrollArea>
196 | 						<TOCItems items={toc} />
197 | 					</TOCScrollArea>
198 | 					{tocOptions.footer}
199 | 				</Toc>,
200 | 				{
201 | 					items: toc,
202 | 					...tocOptions,
203 | 				},
204 | 			)}
205 | 		</AnchorProvider>
206 | 	);
207 | }
208 | 
209 | function EditOnGitHub({
210 | 	owner,
211 | 	repo,
212 | 	sha,
213 | 	path,
214 | 	...props
215 | }: EditOnGitHubOptions) {
216 | 	const href = `https://github.com/${owner}/${repo}/blob/${sha}/${path.startsWith("/") ? path.slice(1) : path}`;
217 | 
218 | 	return (
219 | 		<a
220 | 			href={href}
221 | 			target="_blank"
222 | 			rel="noreferrer noopener"
223 | 			{...props}
224 | 			className={cn(
225 | 				buttonVariants({
226 | 					color: "secondary",
227 | 					className: "gap-1.5 text-fd-muted-foreground",
228 | 				}),
229 | 				props.className,
230 | 			)}
231 | 		>
232 | 			<Edit className="size-3.5" />
233 | 			<I18nLabel label="editOnGithub" />
234 | 		</a>
235 | 	);
236 | }
237 | 
238 | /**
239 |  * Add typography styles
240 |  */
241 | export const DocsBody = forwardRef<
242 | 	HTMLDivElement,
243 | 	HTMLAttributes<HTMLDivElement>
244 | >((props, ref) => (
245 | 	<div ref={ref} {...props} className={cn("prose", props.className)}>
246 | 		{props.children}
247 | 	</div>
248 | ));
249 | 
250 | DocsBody.displayName = "DocsBody";
251 | 
252 | export const DocsDescription = forwardRef<
253 | 	HTMLParagraphElement,
254 | 	HTMLAttributes<HTMLParagraphElement>
255 | >((props, ref) => {
256 | 	// don't render if no description provided
257 | 	if (props.children === undefined) return null;
258 | 
259 | 	return (
260 | 		<p
261 | 			ref={ref}
262 | 			{...props}
263 | 			className={cn("mb-8 text-lg text-fd-muted-foreground", props.className)}
264 | 		>
265 | 			{props.children}
266 | 		</p>
267 | 	);
268 | });
269 | 
270 | DocsDescription.displayName = "DocsDescription";
271 | 
272 | export const DocsTitle = forwardRef<
273 | 	HTMLHeadingElement,
274 | 	HTMLAttributes<HTMLHeadingElement>
275 | >((props, ref) => {
276 | 	return (
277 | 		<h1
278 | 			ref={ref}
279 | 			{...props}
280 | 			className={cn("text-3xl font-semibold", props.className)}
281 | 		>
282 | 			{props.children}
283 | 		</h1>
284 | 	);
285 | });
286 | 
287 | DocsTitle.displayName = "DocsTitle";
288 | 
289 | /**
290 |  * For separate MDX page
291 |  */
292 | export function withArticle({ children }: { children: ReactNode }): ReactNode {
293 | 	return (
294 | 		<main className="container py-12">
295 | 			<article className="prose">{children}</article>
296 | 		</main>
297 | 	);
298 | }
299 | 
```

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

```typescript
  1 | import { APIError } from "better-call";
  2 | import { getSessionFromCtx } from "../../api";
  3 | import type {
  4 | 	AuthorizationQuery,
  5 | 	Client,
  6 | 	OIDCOptions,
  7 | } from "../oidc-provider/types";
  8 | import { generateRandomString } from "../../crypto";
  9 | import type { GenericEndpointContext } from "@better-auth/core";
 10 | 
 11 | function redirectErrorURL(url: string, error: string, description: string) {
 12 | 	return `${
 13 | 		url.includes("?") ? "&" : "?"
 14 | 	}error=${error}&error_description=${description}`;
 15 | }
 16 | 
 17 | export async function authorizeMCPOAuth(
 18 | 	ctx: GenericEndpointContext,
 19 | 	options: OIDCOptions,
 20 | ) {
 21 | 	ctx.setHeader("Access-Control-Allow-Origin", "*");
 22 | 	ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
 23 | 	ctx.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
 24 | 	ctx.setHeader("Access-Control-Max-Age", "86400");
 25 | 	const opts = {
 26 | 		codeExpiresIn: 600,
 27 | 		defaultScope: "openid",
 28 | 		...options,
 29 | 		scopes: [
 30 | 			"openid",
 31 | 			"profile",
 32 | 			"email",
 33 | 			"offline_access",
 34 | 			...(options?.scopes || []),
 35 | 		],
 36 | 	};
 37 | 	if (!ctx.request) {
 38 | 		throw new APIError("UNAUTHORIZED", {
 39 | 			error_description: "request not found",
 40 | 			error: "invalid_request",
 41 | 		});
 42 | 	}
 43 | 	const session = await getSessionFromCtx(ctx);
 44 | 	if (!session) {
 45 | 		/**
 46 | 		 * If the user is not logged in, we need to redirect them to the
 47 | 		 * login page.
 48 | 		 */
 49 | 		await ctx.setSignedCookie(
 50 | 			"oidc_login_prompt",
 51 | 			JSON.stringify(ctx.query),
 52 | 			ctx.context.secret,
 53 | 			{
 54 | 				maxAge: 600,
 55 | 				path: "/",
 56 | 				sameSite: "lax",
 57 | 			},
 58 | 		);
 59 | 		const queryFromURL = ctx.request.url?.split("?")[1]!;
 60 | 		throw ctx.redirect(`${options.loginPage}?${queryFromURL}`);
 61 | 	}
 62 | 
 63 | 	const query = ctx.query as AuthorizationQuery;
 64 | 	if (!query.client_id) {
 65 | 		throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`);
 66 | 	}
 67 | 
 68 | 	if (!query.response_type) {
 69 | 		throw ctx.redirect(
 70 | 			redirectErrorURL(
 71 | 				`${ctx.context.baseURL}/error`,
 72 | 				"invalid_request",
 73 | 				"response_type is required",
 74 | 			),
 75 | 		);
 76 | 	}
 77 | 
 78 | 	const client = await ctx.context.adapter
 79 | 		.findOne<Record<string, any>>({
 80 | 			model: "oauthApplication",
 81 | 			where: [
 82 | 				{
 83 | 					field: "clientId",
 84 | 					value: ctx.query.client_id,
 85 | 				},
 86 | 			],
 87 | 		})
 88 | 		.then((res) => {
 89 | 			if (!res) {
 90 | 				return null;
 91 | 			}
 92 | 			return {
 93 | 				...res,
 94 | 				redirectURLs: res.redirectURLs.split(","),
 95 | 				metadata: res.metadata ? JSON.parse(res.metadata) : {},
 96 | 			} as Client;
 97 | 		});
 98 | 	if (!client) {
 99 | 		throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`);
100 | 	}
101 | 	const redirectURI = client.redirectURLs.find(
102 | 		(url) => url === ctx.query.redirect_uri,
103 | 	);
104 | 
105 | 	if (!redirectURI || !query.redirect_uri) {
106 | 		/**
107 | 		 * show UI error here warning the user that the redirect URI is invalid
108 | 		 */
109 | 		throw new APIError("BAD_REQUEST", {
110 | 			message: "Invalid redirect URI",
111 | 		});
112 | 	}
113 | 	if (client.disabled) {
114 | 		throw ctx.redirect(`${ctx.context.baseURL}/error?error=client_disabled`);
115 | 	}
116 | 
117 | 	if (query.response_type !== "code") {
118 | 		throw ctx.redirect(
119 | 			`${ctx.context.baseURL}/error?error=unsupported_response_type`,
120 | 		);
121 | 	}
122 | 
123 | 	const requestScope =
124 | 		query.scope?.split(" ").filter((s) => s) || opts.defaultScope.split(" ");
125 | 	const invalidScopes = requestScope.filter((scope) => {
126 | 		return !opts.scopes.includes(scope);
127 | 	});
128 | 	if (invalidScopes.length) {
129 | 		throw ctx.redirect(
130 | 			redirectErrorURL(
131 | 				query.redirect_uri,
132 | 				"invalid_scope",
133 | 				`The following scopes are invalid: ${invalidScopes.join(", ")}`,
134 | 			),
135 | 		);
136 | 	}
137 | 
138 | 	if (
139 | 		(!query.code_challenge || !query.code_challenge_method) &&
140 | 		options.requirePKCE
141 | 	) {
142 | 		throw ctx.redirect(
143 | 			redirectErrorURL(
144 | 				query.redirect_uri,
145 | 				"invalid_request",
146 | 				"pkce is required",
147 | 			),
148 | 		);
149 | 	}
150 | 
151 | 	if (!query.code_challenge_method) {
152 | 		query.code_challenge_method = "plain";
153 | 	}
154 | 
155 | 	if (
156 | 		![
157 | 			"s256",
158 | 			options.allowPlainCodeChallengeMethod ? "plain" : "s256",
159 | 		].includes(query.code_challenge_method?.toLowerCase() || "")
160 | 	) {
161 | 		throw ctx.redirect(
162 | 			redirectErrorURL(
163 | 				query.redirect_uri,
164 | 				"invalid_request",
165 | 				"invalid code_challenge method",
166 | 			),
167 | 		);
168 | 	}
169 | 
170 | 	const code = generateRandomString(32, "a-z", "A-Z", "0-9");
171 | 	const codeExpiresInMs = opts.codeExpiresIn * 1000;
172 | 	const expiresAt = new Date(Date.now() + codeExpiresInMs);
173 | 	try {
174 | 		/**
175 | 		 * Save the code in the database
176 | 		 */
177 | 		await ctx.context.internalAdapter.createVerificationValue({
178 | 			value: JSON.stringify({
179 | 				clientId: client.clientId,
180 | 				redirectURI: query.redirect_uri,
181 | 				scope: requestScope,
182 | 				userId: session.user.id,
183 | 				authTime: new Date(session.session.createdAt).getTime(),
184 | 				/**
185 | 				 * If the prompt is set to `consent`, then we need
186 | 				 * to require the user to consent to the scopes.
187 | 				 *
188 | 				 * This means the code now needs to be treated as a
189 | 				 * consent request.
190 | 				 *
191 | 				 * once the user consents, the code will be updated
192 | 				 * with the actual code. This is to prevent the
193 | 				 * client from using the code before the user
194 | 				 * consents.
195 | 				 */
196 | 				requireConsent: query.prompt === "consent",
197 | 				state: query.prompt === "consent" ? query.state : null,
198 | 				codeChallenge: query.code_challenge,
199 | 				codeChallengeMethod: query.code_challenge_method,
200 | 				nonce: query.nonce,
201 | 			}),
202 | 			identifier: code,
203 | 			expiresAt,
204 | 		});
205 | 	} catch (e) {
206 | 		throw ctx.redirect(
207 | 			redirectErrorURL(
208 | 				query.redirect_uri,
209 | 				"server_error",
210 | 				"An error occurred while processing the request",
211 | 			),
212 | 		);
213 | 	}
214 | 
215 | 	// Consent is NOT required - redirect with the code immediately
216 | 	if (query.prompt !== "consent") {
217 | 		const redirectURIWithCode = new URL(redirectURI);
218 | 		redirectURIWithCode.searchParams.set("code", code);
219 | 		redirectURIWithCode.searchParams.set("state", ctx.query.state);
220 | 		throw ctx.redirect(redirectURIWithCode.toString());
221 | 	}
222 | 
223 | 	// Consent is REQUIRED - redirect to consent page or show consent HTML
224 | 	if (options?.consentPage) {
225 | 		// Set cookie to support cookie-based consent flows
226 | 		await ctx.setSignedCookie("oidc_consent_prompt", code, ctx.context.secret, {
227 | 			maxAge: 600,
228 | 			path: "/",
229 | 			sameSite: "lax",
230 | 		});
231 | 
232 | 		// Pass the consent code as a URL parameter to support URL-BASED consent flows
233 | 		const urlParams = new URLSearchParams();
234 | 		urlParams.set("consent_code", code);
235 | 		urlParams.set("client_id", client.clientId);
236 | 		urlParams.set("scope", requestScope.join(" "));
237 | 		const consentURI = `${options.consentPage}?${urlParams.toString()}`;
238 | 
239 | 		throw ctx.redirect(consentURI);
240 | 	}
241 | 
242 | 	// No consent page configured - fall back to direct redirect with code
243 | 	const redirectURIWithCode = new URL(redirectURI);
244 | 	redirectURIWithCode.searchParams.set("code", code);
245 | 	redirectURIWithCode.searchParams.set("state", ctx.query.state);
246 | 	throw ctx.redirect(redirectURIWithCode.toString());
247 | }
248 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/components/ui/context-menu.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import * as React from "react";
  4 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
  5 | import {
  6 | 	CheckIcon,
  7 | 	ChevronRightIcon,
  8 | 	DotFilledIcon,
  9 | } from "@radix-ui/react-icons";
 10 | 
 11 | import { cn } from "@/lib/utils";
 12 | 
 13 | const ContextMenu = ContextMenuPrimitive.Root;
 14 | 
 15 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
 16 | 
 17 | const ContextMenuGroup = ContextMenuPrimitive.Group;
 18 | 
 19 | const ContextMenuPortal = ContextMenuPrimitive.Portal;
 20 | 
 21 | const ContextMenuSub = ContextMenuPrimitive.Sub;
 22 | 
 23 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
 24 | 
 25 | const ContextMenuSubTrigger = ({
 26 | 	ref,
 27 | 	className,
 28 | 	inset,
 29 | 	children,
 30 | 	...props
 31 | }) => (
 32 | 	<ContextMenuPrimitive.SubTrigger
 33 | 		ref={ref}
 34 | 		className={cn(
 35 | 			"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
 36 | 			inset && "pl-8",
 37 | 			className,
 38 | 		)}
 39 | 		{...props}
 40 | 	>
 41 | 		{children}
 42 | 		<ChevronRightIcon className="ml-auto h-4 w-4" />
 43 | 	</ContextMenuPrimitive.SubTrigger>
 44 | );
 45 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
 46 | 
 47 | const ContextMenuSubContent = ({
 48 | 	ref,
 49 | 	className,
 50 | 	...props
 51 | }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> & {
 52 | 	ref: React.RefObject<
 53 | 		React.ElementRef<typeof ContextMenuPrimitive.SubContent>
 54 | 	>;
 55 | }) => (
 56 | 	<ContextMenuPrimitive.SubContent
 57 | 		ref={ref}
 58 | 		className={cn(
 59 | 			"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
 60 | 			className,
 61 | 		)}
 62 | 		{...props}
 63 | 	/>
 64 | );
 65 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
 66 | 
 67 | const ContextMenuContent = ({
 68 | 	ref,
 69 | 	className,
 70 | 	...props
 71 | }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> & {
 72 | 	ref: React.RefObject<React.ElementRef<typeof ContextMenuPrimitive.Content>>;
 73 | }) => (
 74 | 	<ContextMenuPrimitive.Portal>
 75 | 		<ContextMenuPrimitive.Content
 76 | 			ref={ref}
 77 | 			className={cn(
 78 | 				"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
 79 | 				className,
 80 | 			)}
 81 | 			{...props}
 82 | 		/>
 83 | 	</ContextMenuPrimitive.Portal>
 84 | );
 85 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
 86 | 
 87 | const ContextMenuItem = ({ ref, className, inset, ...props }) => (
 88 | 	<ContextMenuPrimitive.Item
 89 | 		ref={ref}
 90 | 		className={cn(
 91 | 			"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
 92 | 			inset && "pl-8",
 93 | 			className,
 94 | 		)}
 95 | 		{...props}
 96 | 	/>
 97 | );
 98 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
 99 | 
100 | const ContextMenuCheckboxItem = ({
101 | 	ref,
102 | 	className,
103 | 	children,
104 | 	checked,
105 | 	...props
106 | }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & {
107 | 	ref: React.RefObject<
108 | 		React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>
109 | 	>;
110 | }) => (
111 | 	<ContextMenuPrimitive.CheckboxItem
112 | 		ref={ref}
113 | 		className={cn(
114 | 			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
115 | 			className,
116 | 		)}
117 | 		checked={checked}
118 | 		{...props}
119 | 	>
120 | 		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
121 | 			<ContextMenuPrimitive.ItemIndicator>
122 | 				<CheckIcon className="h-4 w-4" />
123 | 			</ContextMenuPrimitive.ItemIndicator>
124 | 		</span>
125 | 		{children}
126 | 	</ContextMenuPrimitive.CheckboxItem>
127 | );
128 | ContextMenuCheckboxItem.displayName =
129 | 	ContextMenuPrimitive.CheckboxItem.displayName;
130 | 
131 | const ContextMenuRadioItem = ({
132 | 	ref,
133 | 	className,
134 | 	children,
135 | 	...props
136 | }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & {
137 | 	ref: React.RefObject<React.ElementRef<typeof ContextMenuPrimitive.RadioItem>>;
138 | }) => (
139 | 	<ContextMenuPrimitive.RadioItem
140 | 		ref={ref}
141 | 		className={cn(
142 | 			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
143 | 			className,
144 | 		)}
145 | 		{...props}
146 | 	>
147 | 		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
148 | 			<ContextMenuPrimitive.ItemIndicator>
149 | 				<DotFilledIcon className="h-4 w-4 fill-current" />
150 | 			</ContextMenuPrimitive.ItemIndicator>
151 | 		</span>
152 | 		{children}
153 | 	</ContextMenuPrimitive.RadioItem>
154 | );
155 | ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
156 | 
157 | const ContextMenuLabel = ({ ref, className, inset, ...props }) => (
158 | 	<ContextMenuPrimitive.Label
159 | 		ref={ref}
160 | 		className={cn(
161 | 			"px-2 py-1.5 text-sm font-semibold text-foreground",
162 | 			inset && "pl-8",
163 | 			className,
164 | 		)}
165 | 		{...props}
166 | 	/>
167 | );
168 | ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
169 | 
170 | const ContextMenuSeparator = ({
171 | 	ref,
172 | 	className,
173 | 	...props
174 | }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> & {
175 | 	ref: React.RefObject<React.ElementRef<typeof ContextMenuPrimitive.Separator>>;
176 | }) => (
177 | 	<ContextMenuPrimitive.Separator
178 | 		ref={ref}
179 | 		className={cn("-mx-1 my-1 h-px bg-border", className)}
180 | 		{...props}
181 | 	/>
182 | );
183 | ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
184 | 
185 | const ContextMenuShortcut = ({
186 | 	className,
187 | 	...props
188 | }: React.HTMLAttributes<HTMLSpanElement>) => {
189 | 	return (
190 | 		<span
191 | 			className={cn(
192 | 				"ml-auto text-xs tracking-widest text-muted-foreground",
193 | 				className,
194 | 			)}
195 | 			{...props}
196 | 		/>
197 | 	);
198 | };
199 | ContextMenuShortcut.displayName = "ContextMenuShortcut";
200 | 
201 | export {
202 | 	ContextMenu,
203 | 	ContextMenuTrigger,
204 | 	ContextMenuContent,
205 | 	ContextMenuItem,
206 | 	ContextMenuCheckboxItem,
207 | 	ContextMenuRadioItem,
208 | 	ContextMenuLabel,
209 | 	ContextMenuSeparator,
210 | 	ContextMenuShortcut,
211 | 	ContextMenuGroup,
212 | 	ContextMenuPortal,
213 | 	ContextMenuSub,
214 | 	ContextMenuSubContent,
215 | 	ContextMenuSubTrigger,
216 | 	ContextMenuRadioGroup,
217 | };
218 | 
```

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

```typescript
  1 | import { logger } from "@better-auth/core/env";
  2 | import { createAdapterFactory } from "../adapter-factory";
  3 | import type { BetterAuthOptions } from "@better-auth/core";
  4 | import type {
  5 | 	DBAdapterDebugLogOption,
  6 | 	CleanedWhere,
  7 | } from "@better-auth/core/db/adapter";
  8 | 
  9 | export interface MemoryDB {
 10 | 	[key: string]: any[];
 11 | }
 12 | 
 13 | export interface MemoryAdapterConfig {
 14 | 	debugLogs?: DBAdapterDebugLogOption;
 15 | }
 16 | 
 17 | export const memoryAdapter = (db: MemoryDB, config?: MemoryAdapterConfig) => {
 18 | 	let lazyOptions: BetterAuthOptions | null = null;
 19 | 	let adapterCreator = createAdapterFactory({
 20 | 		config: {
 21 | 			adapterId: "memory",
 22 | 			adapterName: "Memory Adapter",
 23 | 			usePlural: false,
 24 | 			debugLogs: config?.debugLogs || false,
 25 | 			customTransformInput(props) {
 26 | 				if (
 27 | 					props.options.advanced?.database?.useNumberId &&
 28 | 					props.field === "id" &&
 29 | 					props.action === "create"
 30 | 				) {
 31 | 					return db[props.model]!.length + 1;
 32 | 				}
 33 | 				return props.data;
 34 | 			},
 35 | 			transaction: async (cb) => {
 36 | 				let clone = structuredClone(db);
 37 | 				try {
 38 | 					const r = await cb(adapterCreator(lazyOptions!));
 39 | 					return r;
 40 | 				} catch (error) {
 41 | 					// Rollback changes
 42 | 					Object.keys(db).forEach((key) => {
 43 | 						db[key] = clone[key]!;
 44 | 					});
 45 | 					throw error;
 46 | 				}
 47 | 			},
 48 | 		},
 49 | 		adapter: ({ getFieldName, options, debugLog }) => {
 50 | 			function convertWhereClause(where: CleanedWhere[], model: string) {
 51 | 				const table = db[model];
 52 | 				if (!table) {
 53 | 					logger.error(
 54 | 						`[MemoryAdapter] Model ${model} not found in the DB`,
 55 | 						Object.keys(db),
 56 | 					);
 57 | 					throw new Error(`Model ${model} not found`);
 58 | 				}
 59 | 
 60 | 				const evalClause = (record: any, clause: CleanedWhere): boolean => {
 61 | 					const { field, value, operator } = clause;
 62 | 					switch (operator) {
 63 | 						case "in":
 64 | 							if (!Array.isArray(value)) {
 65 | 								throw new Error("Value must be an array");
 66 | 							}
 67 | 							// @ts-expect-error
 68 | 							return value.includes(record[field]);
 69 | 						case "not_in":
 70 | 							if (!Array.isArray(value)) {
 71 | 								throw new Error("Value must be an array");
 72 | 							}
 73 | 							// @ts-expect-error
 74 | 							return !value.includes(record[field]);
 75 | 						case "contains":
 76 | 							return record[field].includes(value);
 77 | 						case "starts_with":
 78 | 							return record[field].startsWith(value);
 79 | 						case "ends_with":
 80 | 							return record[field].endsWith(value);
 81 | 						case "ne":
 82 | 							return record[field] !== value;
 83 | 						case "gt":
 84 | 							return value != null && Boolean(record[field] > value);
 85 | 						case "gte":
 86 | 							return value != null && Boolean(record[field] >= value);
 87 | 						case "lt":
 88 | 							return value != null && Boolean(record[field] < value);
 89 | 						case "lte":
 90 | 							return value != null && Boolean(record[field] <= value);
 91 | 						default:
 92 | 							return record[field] === value;
 93 | 					}
 94 | 				};
 95 | 
 96 | 				return table.filter((record: any) => {
 97 | 					if (!where.length || where.length === 0) {
 98 | 						return true;
 99 | 					}
100 | 
101 | 					let result = evalClause(record, where[0]!);
102 | 					for (const clause of where) {
103 | 						const clauseResult = evalClause(record, clause);
104 | 
105 | 						if (clause.connector === "OR") {
106 | 							result = result || clauseResult;
107 | 						} else {
108 | 							result = result && clauseResult;
109 | 						}
110 | 					}
111 | 
112 | 					return result;
113 | 				});
114 | 			}
115 | 			return {
116 | 				create: async ({ model, data }) => {
117 | 					if (options.advanced?.database?.useNumberId) {
118 | 						// @ts-expect-error
119 | 						data.id = db[model]!.length + 1;
120 | 					}
121 | 					if (!db[model]) {
122 | 						db[model] = [];
123 | 					}
124 | 					db[model]!.push(data);
125 | 					return data;
126 | 				},
127 | 				findOne: async ({ model, where }) => {
128 | 					const res = convertWhereClause(where, model);
129 | 					const record = res[0] || null;
130 | 					return record;
131 | 				},
132 | 				findMany: async ({ model, where, sortBy, limit, offset }) => {
133 | 					let table = db[model];
134 | 					if (where) {
135 | 						table = convertWhereClause(where, model);
136 | 					}
137 | 					if (sortBy) {
138 | 						table = table!.sort((a, b) => {
139 | 							const field = getFieldName({ model, field: sortBy.field });
140 | 							const aValue = a[field];
141 | 							const bValue = b[field];
142 | 
143 | 							let comparison = 0;
144 | 
145 | 							// Handle null/undefined values
146 | 							if (aValue == null && bValue == null) {
147 | 								comparison = 0;
148 | 							} else if (aValue == null) {
149 | 								comparison = -1;
150 | 							} else if (bValue == null) {
151 | 								comparison = 1;
152 | 							}
153 | 							// Handle string comparison
154 | 							else if (
155 | 								typeof aValue === "string" &&
156 | 								typeof bValue === "string"
157 | 							) {
158 | 								comparison = aValue.localeCompare(bValue);
159 | 							}
160 | 							// Handle date comparison
161 | 							else if (aValue instanceof Date && bValue instanceof Date) {
162 | 								comparison = aValue.getTime() - bValue.getTime();
163 | 							}
164 | 							// Handle numeric comparison
165 | 							else if (
166 | 								typeof aValue === "number" &&
167 | 								typeof bValue === "number"
168 | 							) {
169 | 								comparison = aValue - bValue;
170 | 							}
171 | 							// Handle boolean comparison
172 | 							else if (
173 | 								typeof aValue === "boolean" &&
174 | 								typeof bValue === "boolean"
175 | 							) {
176 | 								comparison = aValue === bValue ? 0 : aValue ? 1 : -1;
177 | 							}
178 | 							// Fallback to string comparison
179 | 							else {
180 | 								comparison = String(aValue).localeCompare(String(bValue));
181 | 							}
182 | 
183 | 							return sortBy.direction === "asc" ? comparison : -comparison;
184 | 						});
185 | 					}
186 | 					if (offset !== undefined) {
187 | 						table = table!.slice(offset);
188 | 					}
189 | 					if (limit !== undefined) {
190 | 						table = table!.slice(0, limit);
191 | 					}
192 | 					return table || [];
193 | 				},
194 | 				count: async ({ model, where }) => {
195 | 					if (where) {
196 | 						const filteredRecords = convertWhereClause(where, model);
197 | 						return filteredRecords.length;
198 | 					}
199 | 					return db[model]!.length;
200 | 				},
201 | 				update: async ({ model, where, update }) => {
202 | 					const res = convertWhereClause(where, model);
203 | 					res.forEach((record) => {
204 | 						Object.assign(record, update);
205 | 					});
206 | 					return res[0] || null;
207 | 				},
208 | 				delete: async ({ model, where }) => {
209 | 					const table = db[model]!;
210 | 					const res = convertWhereClause(where, model);
211 | 					db[model] = table.filter((record) => !res.includes(record));
212 | 				},
213 | 				deleteMany: async ({ model, where }) => {
214 | 					const table = db[model]!;
215 | 					const res = convertWhereClause(where, model);
216 | 					let count = 0;
217 | 					db[model] = table.filter((record) => {
218 | 						if (res.includes(record)) {
219 | 							count++;
220 | 							return false;
221 | 						}
222 | 						return !res.includes(record);
223 | 					});
224 | 					return count;
225 | 				},
226 | 				updateMany({ model, where, update }) {
227 | 					const res = convertWhereClause(where, model);
228 | 					res.forEach((record) => {
229 | 						Object.assign(record, update);
230 | 					});
231 | 					return res[0] || null;
232 | 				},
233 | 			};
234 | 		},
235 | 	});
236 | 	return (options: BetterAuthOptions) => {
237 | 		lazyOptions = options;
238 | 		return adapterCreator(options);
239 | 	};
240 | };
241 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/components/blocks/pricing.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import { Button, buttonVariants } from "@/components/ui/button";
  4 | import { Label } from "@/components/ui/label";
  5 | import { Switch } from "@/components/ui/switch";
  6 | 
  7 | import { cn } from "@/lib/utils";
  8 | import { motion } from "framer-motion";
  9 | import { Star } from "lucide-react";
 10 | import { useState, useRef, useEffect } from "react";
 11 | import confetti from "canvas-confetti";
 12 | import NumberFlow from "@number-flow/react";
 13 | import { CheckIcon } from "@radix-ui/react-icons";
 14 | import { client } from "@/lib/auth-client";
 15 | 
 16 | function useMediaQuery(query: string) {
 17 | 	const [matches, setMatches] = useState(false);
 18 | 
 19 | 	useEffect(() => {
 20 | 		const media = window.matchMedia(query);
 21 | 		if (media.matches !== matches) {
 22 | 			setMatches(media.matches);
 23 | 		}
 24 | 
 25 | 		const listener = () => setMatches(media.matches);
 26 | 		media.addListener(listener);
 27 | 
 28 | 		return () => media.removeListener(listener);
 29 | 	}, [query]);
 30 | 
 31 | 	return matches;
 32 | }
 33 | 
 34 | interface PricingPlan {
 35 | 	name: string;
 36 | 	price: string;
 37 | 	yearlyPrice: string;
 38 | 	period: string;
 39 | 	features: string[];
 40 | 	description: string;
 41 | 	buttonText: string;
 42 | 	href: string;
 43 | 	isPopular: boolean;
 44 | }
 45 | 
 46 | interface PricingProps {
 47 | 	plans: PricingPlan[];
 48 | 	title?: string;
 49 | 	description?: string;
 50 | }
 51 | 
 52 | export function Pricing({
 53 | 	plans,
 54 | 	title = "Simple, Transparent Pricing",
 55 | 	description = "Choose the plan that works for you",
 56 | }: PricingProps) {
 57 | 	const [isMonthly, setIsMonthly] = useState(true);
 58 | 	const isDesktop = useMediaQuery("(min-width: 768px)");
 59 | 	const switchRef = useRef<HTMLButtonElement>(null);
 60 | 
 61 | 	const handleToggle = (checked: boolean) => {
 62 | 		setIsMonthly(!checked);
 63 | 		if (checked && switchRef.current) {
 64 | 			const rect = switchRef.current.getBoundingClientRect();
 65 | 			const x = rect.left + rect.width / 2;
 66 | 			const y = rect.top + rect.height / 2;
 67 | 
 68 | 			confetti({
 69 | 				particleCount: 50,
 70 | 				spread: 60,
 71 | 				origin: {
 72 | 					x: x / window.innerWidth,
 73 | 					y: y / window.innerHeight,
 74 | 				},
 75 | 				colors: [
 76 | 					"hsl(var(--primary))",
 77 | 					"hsl(var(--accent))",
 78 | 					"hsl(var(--secondary))",
 79 | 					"hsl(var(--muted))",
 80 | 				],
 81 | 				ticks: 200,
 82 | 				gravity: 1.2,
 83 | 				decay: 0.94,
 84 | 				startVelocity: 30,
 85 | 				shapes: ["circle"],
 86 | 			});
 87 | 		}
 88 | 	};
 89 | 
 90 | 	return (
 91 | 		<div className="container py-4">
 92 | 			<div className="text-center space-y-4 mb-3">
 93 | 				<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
 94 | 					{title}
 95 | 				</h2>
 96 | 				<p className="text-muted-foreground  whitespace-pre-line">
 97 | 					{description}
 98 | 				</p>
 99 | 			</div>
100 | 
101 | 			<div className="flex justify-center mb-10">
102 | 				<label className="relative inline-flex items-center cursor-pointer">
103 | 					<Label>
104 | 						<Switch
105 | 							ref={switchRef as any}
106 | 							checked={!isMonthly}
107 | 							onCheckedChange={handleToggle}
108 | 							className="relative"
109 | 						/>
110 | 					</Label>
111 | 				</label>
112 | 				<span className="ml-2 font-semibold">
113 | 					Annual billing <span className="text-primary">(Save 20%)</span>
114 | 				</span>
115 | 			</div>
116 | 
117 | 			<div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4">
118 | 				{plans.map((plan, index) => (
119 | 					<motion.div
120 | 						key={index}
121 | 						initial={{ y: 50, opacity: 1 }}
122 | 						whileInView={
123 | 							isDesktop
124 | 								? {
125 | 										y: plan.isPopular ? -20 : 0,
126 | 										opacity: 1,
127 | 										x: index === 2 ? -30 : index === 0 ? 30 : 0,
128 | 										scale: index === 0 || index === 2 ? 0.94 : 1.0,
129 | 									}
130 | 								: {}
131 | 						}
132 | 						viewport={{ once: true }}
133 | 						transition={{
134 | 							duration: 1.6,
135 | 							type: "spring",
136 | 							stiffness: 100,
137 | 							damping: 30,
138 | 							delay: 0.4,
139 | 							opacity: { duration: 0.5 },
140 | 						}}
141 | 						className={cn(
142 | 							`rounded-sm border p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`,
143 | 							plan.isPopular ? "border-border border-2" : "border-border",
144 | 							"flex flex-col",
145 | 							!plan.isPopular && "mt-5",
146 | 							index === 0 || index === 2
147 | 								? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-10"
148 | 								: "z-10",
149 | 							index === 0 && "origin-right",
150 | 							index === 2 && "origin-left",
151 | 						)}
152 | 					>
153 | 						{plan.isPopular && (
154 | 							<div className="absolute top-0 right-0 bg-primary py-0.5 px-2 rounded-bl-sm rounded-tr-sm flex items-center">
155 | 								<Star className="text-primary-foreground h-4 w-4 fill-current" />
156 | 								<span className="text-primary-foreground ml-1 font-sans font-semibold">
157 | 									Popular
158 | 								</span>
159 | 							</div>
160 | 						)}
161 | 						<div className="flex-1 flex flex-col">
162 | 							<p className="text-base font-semibold text-muted-foreground mt-2">
163 | 								{plan.name}
164 | 							</p>
165 | 							<div className="mt-6 flex items-center justify-center gap-x-2">
166 | 								<span className="text-5xl font-bold tracking-tight text-foreground">
167 | 									<NumberFlow
168 | 										value={
169 | 											isMonthly ? Number(plan.price) : Number(plan.yearlyPrice)
170 | 										}
171 | 										format={{
172 | 											style: "currency",
173 | 											currency: "USD",
174 | 											minimumFractionDigits: 0,
175 | 											maximumFractionDigits: 0,
176 | 										}}
177 | 										transformTiming={{
178 | 											duration: 500,
179 | 											easing: "ease-out",
180 | 										}}
181 | 										willChange
182 | 										className="font-variant-numeric: tabular-nums"
183 | 									/>
184 | 								</span>
185 | 								{plan.period !== "Next 3 months" && (
186 | 									<span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground">
187 | 										/ {plan.period}
188 | 									</span>
189 | 								)}
190 | 							</div>
191 | 
192 | 							<p className="text-xs leading-5 text-muted-foreground">
193 | 								{isMonthly ? "billed monthly" : "billed annually"}
194 | 							</p>
195 | 
196 | 							<ul className="mt-5 gap-2 flex flex-col">
197 | 								{plan.features.map((feature, idx) => (
198 | 									<li key={idx} className="flex items-start gap-2">
199 | 										<CheckIcon className="h-4 w-4 text-primary mt-1 shrink-0" />
200 | 										<span className="text-left">{feature}</span>
201 | 									</li>
202 | 								))}
203 | 							</ul>
204 | 
205 | 							<hr className="w-full my-4" />
206 | 							<Button
207 | 								onClick={async () => {
208 | 									await client.subscription.upgrade({
209 | 										plan: plan.name.toLowerCase(),
210 | 										successUrl: "/dashboard",
211 | 									});
212 | 								}}
213 | 								className={cn(
214 | 									buttonVariants({
215 | 										variant: "outline",
216 | 									}),
217 | 									"group relative w-full gap-2 overflow-hidden text-lg font-semibold tracking-tighter",
218 | 									"transform-gpu ring-offset-current transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-1 hover:bg-primary hover:text-primary-foreground",
219 | 									plan.isPopular
220 | 										? "bg-primary text-primary-foreground"
221 | 										: "bg-background text-foreground",
222 | 								)}
223 | 							>
224 | 								{plan.buttonText}
225 | 							</Button>
226 | 							<p className="mt-6 text-xs leading-5 text-muted-foreground">
227 | 								{plan.description}
228 | 							</p>
229 | 						</div>
230 | 					</motion.div>
231 | 				))}
232 | 			</div>
233 | 		</div>
234 | 	);
235 | }
236 | 
```

--------------------------------------------------------------------------------
/packages/core/src/social-providers/cognito.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { betterFetch } from "@better-fetch/fetch";
  2 | import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
  3 | import { BetterAuthError } from "../error";
  4 | import type { OAuthProvider, ProviderOptions } from "../oauth2";
  5 | import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
  6 | import { logger } from "../env";
  7 | import { refreshAccessToken } from "../oauth2";
  8 | import { APIError } from "better-call";
  9 | 
 10 | export interface CognitoProfile {
 11 | 	sub: string;
 12 | 	email: string;
 13 | 	email_verified: boolean;
 14 | 	name: string;
 15 | 	given_name?: string;
 16 | 	family_name?: string;
 17 | 	picture?: string;
 18 | 	username?: string;
 19 | 	locale?: string;
 20 | 	phone_number?: string;
 21 | 	phone_number_verified?: boolean;
 22 | 	aud: string;
 23 | 	iss: string;
 24 | 	exp: number;
 25 | 	iat: number;
 26 | 	// Custom attributes from Cognito can be added here
 27 | 	[key: string]: any;
 28 | }
 29 | 
 30 | export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
 31 | 	clientId: string;
 32 | 	/**
 33 | 	 * The Cognito domain (e.g., "your-app.auth.us-east-1.amazoncognito.com")
 34 | 	 */
 35 | 	domain: string;
 36 | 	/**
 37 | 	 * AWS region where User Pool is hosted (e.g., "us-east-1")
 38 | 	 */
 39 | 	region: string;
 40 | 	userPoolId: string;
 41 | 	requireClientSecret?: boolean;
 42 | }
 43 | 
 44 | export const cognito = (options: CognitoOptions) => {
 45 | 	if (!options.domain || !options.region || !options.userPoolId) {
 46 | 		logger.error(
 47 | 			"Domain, region and userPoolId are required for Amazon Cognito. Make sure to provide them in the options.",
 48 | 		);
 49 | 		throw new BetterAuthError("DOMAIN_AND_REGION_REQUIRED");
 50 | 	}
 51 | 
 52 | 	const cleanDomain = options.domain.replace(/^https?:\/\//, "");
 53 | 	const authorizationEndpoint = `https://${cleanDomain}/oauth2/authorize`;
 54 | 	const tokenEndpoint = `https://${cleanDomain}/oauth2/token`;
 55 | 	const userInfoEndpoint = `https://${cleanDomain}/oauth2/userinfo`;
 56 | 
 57 | 	return {
 58 | 		id: "cognito",
 59 | 		name: "Cognito",
 60 | 		async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
 61 | 			if (!options.clientId) {
 62 | 				logger.error(
 63 | 					"ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
 64 | 				);
 65 | 				throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
 66 | 			}
 67 | 
 68 | 			if (options.requireClientSecret && !options.clientSecret) {
 69 | 				logger.error(
 70 | 					"Client Secret is required when requireClientSecret is true. Make sure to provide it in the options.",
 71 | 				);
 72 | 				throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
 73 | 			}
 74 | 			const _scopes = options.disableDefaultScope
 75 | 				? []
 76 | 				: ["openid", "profile", "email"];
 77 | 			options.scope && _scopes.push(...options.scope);
 78 | 			scopes && _scopes.push(...scopes);
 79 | 
 80 | 			const url = await createAuthorizationURL({
 81 | 				id: "cognito",
 82 | 				options: {
 83 | 					...options,
 84 | 				},
 85 | 				authorizationEndpoint,
 86 | 				scopes: _scopes,
 87 | 				state,
 88 | 				codeVerifier,
 89 | 				redirectURI,
 90 | 				prompt: options.prompt,
 91 | 			});
 92 | 			return url;
 93 | 		},
 94 | 
 95 | 		validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
 96 | 			return validateAuthorizationCode({
 97 | 				code,
 98 | 				codeVerifier,
 99 | 				redirectURI,
100 | 				options,
101 | 				tokenEndpoint,
102 | 			});
103 | 		},
104 | 
105 | 		refreshAccessToken: options.refreshAccessToken
106 | 			? options.refreshAccessToken
107 | 			: async (refreshToken) => {
108 | 					return refreshAccessToken({
109 | 						refreshToken,
110 | 						options: {
111 | 							clientId: options.clientId,
112 | 							clientKey: options.clientKey,
113 | 							clientSecret: options.clientSecret,
114 | 						},
115 | 						tokenEndpoint,
116 | 					});
117 | 				},
118 | 
119 | 		async verifyIdToken(token, nonce) {
120 | 			if (options.disableIdTokenSignIn) {
121 | 				return false;
122 | 			}
123 | 			if (options.verifyIdToken) {
124 | 				return options.verifyIdToken(token, nonce);
125 | 			}
126 | 
127 | 			try {
128 | 				const decodedHeader = decodeProtectedHeader(token);
129 | 				const { kid, alg: jwtAlg } = decodedHeader;
130 | 				if (!kid || !jwtAlg) return false;
131 | 
132 | 				const publicKey = await getCognitoPublicKey(
133 | 					kid,
134 | 					options.region,
135 | 					options.userPoolId,
136 | 				);
137 | 				const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
138 | 
139 | 				const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
140 | 					algorithms: [jwtAlg],
141 | 					issuer: expectedIssuer,
142 | 					audience: options.clientId,
143 | 					maxTokenAge: "1h",
144 | 				});
145 | 
146 | 				if (nonce && jwtClaims.nonce !== nonce) {
147 | 					return false;
148 | 				}
149 | 				return true;
150 | 			} catch (error) {
151 | 				logger.error("Failed to verify ID token:", error);
152 | 				return false;
153 | 			}
154 | 		},
155 | 
156 | 		async getUserInfo(token) {
157 | 			if (options.getUserInfo) {
158 | 				return options.getUserInfo(token);
159 | 			}
160 | 
161 | 			if (token.idToken) {
162 | 				try {
163 | 					const profile = decodeJwt<CognitoProfile>(token.idToken);
164 | 					if (!profile) {
165 | 						return null;
166 | 					}
167 | 					const name =
168 | 						profile.name ||
169 | 						profile.given_name ||
170 | 						profile.username ||
171 | 						profile.email;
172 | 					const enrichedProfile = {
173 | 						...profile,
174 | 						name,
175 | 					};
176 | 					const userMap = await options.mapProfileToUser?.(enrichedProfile);
177 | 
178 | 					return {
179 | 						user: {
180 | 							id: profile.sub,
181 | 							name: enrichedProfile.name,
182 | 							email: profile.email,
183 | 							image: profile.picture,
184 | 							emailVerified: profile.email_verified,
185 | 							...userMap,
186 | 						},
187 | 						data: enrichedProfile,
188 | 					};
189 | 				} catch (error) {
190 | 					logger.error("Failed to decode ID token:", error);
191 | 				}
192 | 			}
193 | 
194 | 			if (token.accessToken) {
195 | 				try {
196 | 					const { data: userInfo } = await betterFetch<CognitoProfile>(
197 | 						userInfoEndpoint,
198 | 						{
199 | 							headers: {
200 | 								Authorization: `Bearer ${token.accessToken}`,
201 | 							},
202 | 						},
203 | 					);
204 | 
205 | 					if (userInfo) {
206 | 						const userMap = await options.mapProfileToUser?.(userInfo);
207 | 						return {
208 | 							user: {
209 | 								id: userInfo.sub,
210 | 								name: userInfo.name || userInfo.given_name || userInfo.username,
211 | 								email: userInfo.email,
212 | 								image: userInfo.picture,
213 | 								emailVerified: userInfo.email_verified,
214 | 								...userMap,
215 | 							},
216 | 							data: userInfo,
217 | 						};
218 | 					}
219 | 				} catch (error) {
220 | 					logger.error("Failed to fetch user info from Cognito:", error);
221 | 				}
222 | 			}
223 | 
224 | 			return null;
225 | 		},
226 | 
227 | 		options,
228 | 	} satisfies OAuthProvider<CognitoProfile>;
229 | };
230 | 
231 | export const getCognitoPublicKey = async (
232 | 	kid: string,
233 | 	region: string,
234 | 	userPoolId: string,
235 | ) => {
236 | 	const COGNITO_JWKS_URI = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
237 | 
238 | 	try {
239 | 		const { data } = await betterFetch<{
240 | 			keys: Array<{
241 | 				kid: string;
242 | 				alg: string;
243 | 				kty: string;
244 | 				use: string;
245 | 				n: string;
246 | 				e: string;
247 | 			}>;
248 | 		}>(COGNITO_JWKS_URI);
249 | 
250 | 		if (!data?.keys) {
251 | 			throw new APIError("BAD_REQUEST", {
252 | 				message: "Keys not found",
253 | 			});
254 | 		}
255 | 
256 | 		const jwk = data.keys.find((key) => key.kid === kid);
257 | 		if (!jwk) {
258 | 			throw new Error(`JWK with kid ${kid} not found`);
259 | 		}
260 | 
261 | 		return await importJWK(jwk, jwk.alg);
262 | 	} catch (error) {
263 | 		logger.error("Failed to fetch Cognito public key:", error);
264 | 		throw error;
265 | 	}
266 | };
267 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/api-key/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { InferOptionSchema } from "../../types";
  2 | import type { Statements } from "../access";
  3 | import type { apiKeySchema } from "./schema";
  4 | import type {
  5 | 	GenericEndpointContext,
  6 | 	HookEndpointContext,
  7 | } from "@better-auth/core";
  8 | export interface ApiKeyOptions {
  9 | 	/**
 10 | 	 * The header name to check for API key
 11 | 	 * @default "x-api-key"
 12 | 	 */
 13 | 	apiKeyHeaders?: string | string[];
 14 | 	/**
 15 | 	 * Disable hashing of the API key.
 16 | 	 *
 17 | 	 * ⚠️ Security Warning: It's strongly recommended to not disable hashing.
 18 | 	 * Storing API keys in plaintext makes them vulnerable to database breaches, potentially exposing all your users' API keys.
 19 | 	 *
 20 | 	 * @default false
 21 | 	 */
 22 | 	disableKeyHashing?: boolean;
 23 | 	/**
 24 | 	 * The function to get the API key from the context
 25 | 	 */
 26 | 	customAPIKeyGetter?: (ctx: HookEndpointContext) => string | null;
 27 | 	/**
 28 | 	 * A custom function to validate the API key
 29 | 	 */
 30 | 	customAPIKeyValidator?: (options: {
 31 | 		ctx: GenericEndpointContext;
 32 | 		key: string;
 33 | 	}) => boolean | Promise<boolean>;
 34 | 	/**
 35 | 	 * custom key generation function
 36 | 	 */
 37 | 	customKeyGenerator?: (options: {
 38 | 		/**
 39 | 		 * The length of the API key to generate
 40 | 		 */
 41 | 		length: number;
 42 | 		/**
 43 | 		 * The prefix of the API key to generate
 44 | 		 */
 45 | 		prefix: string | undefined;
 46 | 	}) => string | Promise<string>;
 47 | 	/**
 48 | 	 * The configuration for storing the starting characters of the API key in the database.
 49 | 	 *
 50 | 	 * Useful if you want to display the starting characters of an API key in the UI.
 51 | 	 */
 52 | 	startingCharactersConfig?: {
 53 | 		/**
 54 | 		 * Whether to store the starting characters in the database. If false, we will set `start` to `null`.
 55 | 		 *
 56 | 		 * @default true
 57 | 		 */
 58 | 		shouldStore?: boolean;
 59 | 		/**
 60 | 		 * The length of the starting characters to store in the database.
 61 | 		 *
 62 | 		 * This includes the prefix length.
 63 | 		 *
 64 | 		 * @default 6
 65 | 		 */
 66 | 		charactersLength?: number;
 67 | 	};
 68 | 	/**
 69 | 	 * The length of the API key. Longer is better. Default is 64. (Doesn't include the prefix length)
 70 | 	 * @default 64
 71 | 	 */
 72 | 	defaultKeyLength?: number;
 73 | 	/**
 74 | 	 * The prefix of the API key.
 75 | 	 *
 76 | 	 * Note: We recommend you append an underscore to the prefix to make the prefix more identifiable. (eg `hello_`)
 77 | 	 */
 78 | 	defaultPrefix?: string;
 79 | 	/**
 80 | 	 * The maximum length of the prefix.
 81 | 	 *
 82 | 	 * @default 32
 83 | 	 */
 84 | 	maximumPrefixLength?: number;
 85 | 	/**
 86 | 	 * Whether to require a name for the API key.
 87 | 	 *
 88 | 	 * @default false
 89 | 	 */
 90 | 	requireName?: boolean;
 91 | 	/**
 92 | 	 * The minimum length of the prefix.
 93 | 	 *
 94 | 	 * @default 1
 95 | 	 */
 96 | 	minimumPrefixLength?: number;
 97 | 	/**
 98 | 	 * The maximum length of the name.
 99 | 	 *
100 | 	 * @default 32
101 | 	 */
102 | 	maximumNameLength?: number;
103 | 	/**
104 | 	 * The minimum length of the name.
105 | 	 *
106 | 	 * @default 1
107 | 	 */
108 | 	minimumNameLength?: number;
109 | 	/**
110 | 	 * Whether to enable metadata for an API key.
111 | 	 *
112 | 	 * @default false
113 | 	 */
114 | 	enableMetadata?: boolean;
115 | 	/**
116 | 	 * Customize the key expiration.
117 | 	 */
118 | 	keyExpiration?: {
119 | 		/**
120 | 		 * The default expires time in milliseconds.
121 | 		 *
122 | 		 * If `null`, then there will be no expiration time.
123 | 		 *
124 | 		 * @default null
125 | 		 */
126 | 		defaultExpiresIn?: number | null;
127 | 		/**
128 | 		 * Whether to disable the expires time passed from the client.
129 | 		 *
130 | 		 * If `true`, the expires time will be based on the default values.
131 | 		 *
132 | 		 * @default false
133 | 		 */
134 | 		disableCustomExpiresTime?: boolean;
135 | 		/**
136 | 		 * The minimum expiresIn value allowed to be set from the client. in days.
137 | 		 *
138 | 		 * @default 1
139 | 		 */
140 | 		minExpiresIn?: number;
141 | 		/**
142 | 		 * The maximum expiresIn value allowed to be set from the client. in days.
143 | 		 *
144 | 		 * @default 365
145 | 		 */
146 | 		maxExpiresIn?: number;
147 | 	};
148 | 	/**
149 | 	 * Default rate limiting options.
150 | 	 */
151 | 	rateLimit?: {
152 | 		/**
153 | 		 * Whether to enable rate limiting.
154 | 		 *
155 | 		 * @default true
156 | 		 */
157 | 		enabled?: boolean;
158 | 		/**
159 | 		 * The duration in milliseconds where each request is counted.
160 | 		 *
161 | 		 * Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
162 | 		 *
163 | 		 * @default 1000 * 60 * 60 * 24 // 1 day
164 | 		 */
165 | 		timeWindow?: number;
166 | 		/**
167 | 		 * Maximum amount of requests allowed within a window
168 | 		 *
169 | 		 * Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
170 | 		 *
171 | 		 * @default 10 // 10 requests per day
172 | 		 */
173 | 		maxRequests?: number;
174 | 	};
175 | 	/**
176 | 	 * custom schema for the API key plugin
177 | 	 */
178 | 	schema?: InferOptionSchema<ReturnType<typeof apiKeySchema>>;
179 | 	/**
180 | 	 * An API Key can represent a valid session, so we automatically mock a session for the user if we find a valid API key in the request headers.
181 | 	 *
182 | 	 * ⚠︎ This is not recommended for production use, as it can lead to security issues.
183 | 	 * @default false
184 | 	 */
185 | 	enableSessionForAPIKeys?: boolean;
186 | 	/**
187 | 	 * Permissions for the API key.
188 | 	 */
189 | 	permissions?: {
190 | 		/**
191 | 		 * The default permissions for the API key.
192 | 		 */
193 | 		defaultPermissions?:
194 | 			| Statements
195 | 			| ((
196 | 					userId: string,
197 | 					ctx: GenericEndpointContext,
198 | 			  ) => Statements | Promise<Statements>);
199 | 	};
200 | }
201 | 
202 | export type ApiKey = {
203 | 	/**
204 | 	 * ID
205 | 	 */
206 | 	id: string;
207 | 	/**
208 | 	 * The name of the key
209 | 	 */
210 | 	name: string | null;
211 | 	/**
212 | 	 * Shows the first few characters of the API key, including the prefix.
213 | 	 * This allows you to show those few characters in the UI to make it easier for users to identify the API key.
214 | 	 */
215 | 	start: string | null;
216 | 	/**
217 | 	 * The API Key prefix. Stored as plain text.
218 | 	 */
219 | 	prefix: string | null;
220 | 	/**
221 | 	 * The hashed API key value
222 | 	 */
223 | 	key: string;
224 | 	/**
225 | 	 * The owner of the user id
226 | 	 */
227 | 	userId: string;
228 | 	/**
229 | 	 * The interval in milliseconds between refills of the `remaining` count
230 | 	 *
231 | 	 * @example 3600000 // refill every hour (3600000ms = 1h)
232 | 	 */
233 | 	refillInterval: number | null;
234 | 	/**
235 | 	 * The amount to refill
236 | 	 */
237 | 	refillAmount: number | null;
238 | 	/**
239 | 	 * The last refill date
240 | 	 */
241 | 	lastRefillAt: Date | null;
242 | 	/**
243 | 	 * Sets if key is enabled or disabled
244 | 	 *
245 | 	 * @default true
246 | 	 */
247 | 	enabled: boolean;
248 | 	/**
249 | 	 * Whether the key has rate limiting enabled.
250 | 	 */
251 | 	rateLimitEnabled: boolean;
252 | 	/**
253 | 	 * The duration in milliseconds
254 | 	 */
255 | 	rateLimitTimeWindow: number | null;
256 | 	/**
257 | 	 * Maximum amount of requests allowed within a window
258 | 	 */
259 | 	rateLimitMax: number | null;
260 | 	/**
261 | 	 * The number of requests made within the rate limit time window
262 | 	 */
263 | 	requestCount: number;
264 | 	/**
265 | 	 * Remaining requests (every time API key is used this should updated and should be updated on refill as well)
266 | 	 */
267 | 	remaining: number | null;
268 | 	/**
269 | 	 * When last request occurred
270 | 	 */
271 | 	lastRequest: Date | null;
272 | 	/**
273 | 	 * Expiry date of a key
274 | 	 */
275 | 	expiresAt: Date | null;
276 | 	/**
277 | 	 * created at
278 | 	 */
279 | 	createdAt: Date;
280 | 	/**
281 | 	 * updated at
282 | 	 */
283 | 	updatedAt: Date;
284 | 	/**
285 | 	 * Extra metadata about the apiKey
286 | 	 */
287 | 	metadata: Record<string, any> | null;
288 | 	/**
289 | 	 * Permissions for the API key
290 | 	 */
291 | 	permissions?: {
292 | 		[key: string]: string[];
293 | 	} | null;
294 | };
295 | 
```

--------------------------------------------------------------------------------
/packages/core/src/social-providers/paypal.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { betterFetch } from "@better-fetch/fetch";
  2 | import { BetterAuthError } from "../error";
  3 | import type { OAuthProvider, ProviderOptions } from "../oauth2";
  4 | import { createAuthorizationURL } from "../oauth2";
  5 | import { logger } from "../env";
  6 | import { decodeJwt } from "jose";
  7 | import { base64 } from "@better-auth/utils/base64";
  8 | 
  9 | export interface PayPalProfile {
 10 | 	user_id: string;
 11 | 	name: string;
 12 | 	given_name: string;
 13 | 	family_name: string;
 14 | 	middle_name?: string;
 15 | 	picture?: string;
 16 | 	email: string;
 17 | 	email_verified: boolean;
 18 | 	gender?: string;
 19 | 	birthdate?: string;
 20 | 	zoneinfo?: string;
 21 | 	locale?: string;
 22 | 	phone_number?: string;
 23 | 	address?: {
 24 | 		street_address?: string;
 25 | 		locality?: string;
 26 | 		region?: string;
 27 | 		postal_code?: string;
 28 | 		country?: string;
 29 | 	};
 30 | 	verified_account?: boolean;
 31 | 	account_type?: string;
 32 | 	age_range?: string;
 33 | 	payer_id?: string;
 34 | }
 35 | 
 36 | export interface PayPalTokenResponse {
 37 | 	scope?: string;
 38 | 	access_token: string;
 39 | 	refresh_token?: string;
 40 | 	token_type: "Bearer";
 41 | 	id_token?: string;
 42 | 	expires_in: number;
 43 | 	nonce?: string;
 44 | }
 45 | 
 46 | export interface PayPalOptions extends ProviderOptions<PayPalProfile> {
 47 | 	clientId: string;
 48 | 	/**
 49 | 	 * PayPal environment - 'sandbox' for testing, 'live' for production
 50 | 	 * @default 'sandbox'
 51 | 	 */
 52 | 	environment?: "sandbox" | "live";
 53 | 	/**
 54 | 	 * Whether to request shipping address information
 55 | 	 * @default false
 56 | 	 */
 57 | 	requestShippingAddress?: boolean;
 58 | }
 59 | 
 60 | export const paypal = (options: PayPalOptions) => {
 61 | 	const environment = options.environment || "sandbox";
 62 | 	const isSandbox = environment === "sandbox";
 63 | 
 64 | 	const authorizationEndpoint = isSandbox
 65 | 		? "https://www.sandbox.paypal.com/signin/authorize"
 66 | 		: "https://www.paypal.com/signin/authorize";
 67 | 
 68 | 	const tokenEndpoint = isSandbox
 69 | 		? "https://api-m.sandbox.paypal.com/v1/oauth2/token"
 70 | 		: "https://api-m.paypal.com/v1/oauth2/token";
 71 | 
 72 | 	const userInfoEndpoint = isSandbox
 73 | 		? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo"
 74 | 		: "https://api-m.paypal.com/v1/identity/oauth2/userinfo";
 75 | 
 76 | 	return {
 77 | 		id: "paypal",
 78 | 		name: "PayPal",
 79 | 		async createAuthorizationURL({ state, codeVerifier, redirectURI }) {
 80 | 			if (!options.clientId || !options.clientSecret) {
 81 | 				logger.error(
 82 | 					"Client Id and Client Secret is required for PayPal. Make sure to provide them in the options.",
 83 | 				);
 84 | 				throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
 85 | 			}
 86 | 
 87 | 			/**
 88 | 			 * Log in with PayPal doesn't use traditional OAuth2 scopes
 89 | 			 * Instead, permissions are configured in the PayPal Developer Dashboard
 90 | 			 * We don't pass any scopes to avoid "invalid scope" errors
 91 | 			 **/
 92 | 
 93 | 			const _scopes: string[] = [];
 94 | 
 95 | 			const url = await createAuthorizationURL({
 96 | 				id: "paypal",
 97 | 				options,
 98 | 				authorizationEndpoint,
 99 | 				scopes: _scopes,
100 | 				state,
101 | 				codeVerifier,
102 | 				redirectURI,
103 | 				prompt: options.prompt,
104 | 			});
105 | 			return url;
106 | 		},
107 | 
108 | 		validateAuthorizationCode: async ({ code, redirectURI }) => {
109 | 			/**
110 | 			 * PayPal requires Basic Auth for token exchange
111 | 			 **/
112 | 
113 | 			const credentials = base64.encode(
114 | 				`${options.clientId}:${options.clientSecret}`,
115 | 			);
116 | 
117 | 			try {
118 | 				const response = await betterFetch(tokenEndpoint, {
119 | 					method: "POST",
120 | 					headers: {
121 | 						Authorization: `Basic ${credentials}`,
122 | 						Accept: "application/json",
123 | 						"Accept-Language": "en_US",
124 | 						"Content-Type": "application/x-www-form-urlencoded",
125 | 					},
126 | 					body: new URLSearchParams({
127 | 						grant_type: "authorization_code",
128 | 						code: code,
129 | 						redirect_uri: redirectURI,
130 | 					}).toString(),
131 | 				});
132 | 
133 | 				if (!response.data) {
134 | 					throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
135 | 				}
136 | 
137 | 				const data = response.data as PayPalTokenResponse;
138 | 
139 | 				const result = {
140 | 					accessToken: data.access_token,
141 | 					refreshToken: data.refresh_token,
142 | 					accessTokenExpiresAt: data.expires_in
143 | 						? new Date(Date.now() + data.expires_in * 1000)
144 | 						: undefined,
145 | 					idToken: data.id_token,
146 | 				};
147 | 
148 | 				return result;
149 | 			} catch (error) {
150 | 				logger.error("PayPal token exchange failed:", error);
151 | 				throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
152 | 			}
153 | 		},
154 | 
155 | 		refreshAccessToken: options.refreshAccessToken
156 | 			? options.refreshAccessToken
157 | 			: async (refreshToken) => {
158 | 					const credentials = base64.encode(
159 | 						`${options.clientId}:${options.clientSecret}`,
160 | 					);
161 | 
162 | 					try {
163 | 						const response = await betterFetch(tokenEndpoint, {
164 | 							method: "POST",
165 | 							headers: {
166 | 								Authorization: `Basic ${credentials}`,
167 | 								Accept: "application/json",
168 | 								"Accept-Language": "en_US",
169 | 								"Content-Type": "application/x-www-form-urlencoded",
170 | 							},
171 | 							body: new URLSearchParams({
172 | 								grant_type: "refresh_token",
173 | 								refresh_token: refreshToken,
174 | 							}).toString(),
175 | 						});
176 | 
177 | 						if (!response.data) {
178 | 							throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
179 | 						}
180 | 
181 | 						const data = response.data as any;
182 | 						return {
183 | 							accessToken: data.access_token,
184 | 							refreshToken: data.refresh_token,
185 | 							accessTokenExpiresAt: data.expires_in
186 | 								? new Date(Date.now() + data.expires_in * 1000)
187 | 								: undefined,
188 | 						};
189 | 					} catch (error) {
190 | 						logger.error("PayPal token refresh failed:", error);
191 | 						throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
192 | 					}
193 | 				},
194 | 
195 | 		async verifyIdToken(token, nonce) {
196 | 			if (options.disableIdTokenSignIn) {
197 | 				return false;
198 | 			}
199 | 			if (options.verifyIdToken) {
200 | 				return options.verifyIdToken(token, nonce);
201 | 			}
202 | 			try {
203 | 				const payload = decodeJwt(token);
204 | 				return !!payload.sub;
205 | 			} catch (error) {
206 | 				logger.error("Failed to verify PayPal ID token:", error);
207 | 				return false;
208 | 			}
209 | 		},
210 | 
211 | 		async getUserInfo(token) {
212 | 			if (options.getUserInfo) {
213 | 				return options.getUserInfo(token);
214 | 			}
215 | 
216 | 			if (!token.accessToken) {
217 | 				logger.error("Access token is required to fetch PayPal user info");
218 | 				return null;
219 | 			}
220 | 
221 | 			try {
222 | 				const response = await betterFetch<PayPalProfile>(
223 | 					`${userInfoEndpoint}?schema=paypalv1.1`,
224 | 					{
225 | 						headers: {
226 | 							Authorization: `Bearer ${token.accessToken}`,
227 | 							Accept: "application/json",
228 | 						},
229 | 					},
230 | 				);
231 | 
232 | 				if (!response.data) {
233 | 					logger.error("Failed to fetch user info from PayPal");
234 | 					return null;
235 | 				}
236 | 
237 | 				const userInfo = response.data;
238 | 				const userMap = await options.mapProfileToUser?.(userInfo);
239 | 
240 | 				const result = {
241 | 					user: {
242 | 						id: userInfo.user_id,
243 | 						name: userInfo.name,
244 | 						email: userInfo.email,
245 | 						image: userInfo.picture,
246 | 						emailVerified: userInfo.email_verified,
247 | 						...userMap,
248 | 					},
249 | 					data: userInfo,
250 | 				};
251 | 
252 | 				return result;
253 | 			} catch (error) {
254 | 				logger.error("Failed to fetch user info from PayPal:", error);
255 | 				return null;
256 | 			}
257 | 		},
258 | 
259 | 		options,
260 | 	} satisfies OAuthProvider<PayPalProfile>;
261 | };
262 | 
```
Page 20/69FirstPrevNextLast