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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/kysely-adapter/bun-sqlite-dialect.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @see {@link https://github.com/dylanblokhuis/kysely-bun-sqlite} - Fork of the original kysely-bun-sqlite package by @dylanblokhuis
  3 |  */
  4 | import {
  5 | 	Kysely,
  6 | 	CompiledQuery,
  7 | 	DEFAULT_MIGRATION_LOCK_TABLE,
  8 | 	DEFAULT_MIGRATION_TABLE,
  9 | 	sql,
 10 | 	type DatabaseConnection,
 11 | 	type QueryResult,
 12 | 	type DatabaseIntrospector,
 13 | 	type SchemaMetadata,
 14 | 	type DatabaseMetadataOptions,
 15 | 	type TableMetadata,
 16 | 	type DatabaseMetadata,
 17 | 	type Driver,
 18 | 	type Dialect,
 19 | 	type QueryCompiler,
 20 | 	type DialectAdapter,
 21 | } from "kysely";
 22 | import { DefaultQueryCompiler } from "kysely";
 23 | import { DialectAdapterBase } from "kysely";
 24 | import type { Database } from "bun:sqlite";
 25 | 
 26 | export class BunSqliteAdapter implements DialectAdapterBase {
 27 | 	get supportsCreateIfNotExists(): boolean {
 28 | 		return true;
 29 | 	}
 30 | 
 31 | 	get supportsTransactionalDdl(): boolean {
 32 | 		return false;
 33 | 	}
 34 | 
 35 | 	get supportsReturning(): boolean {
 36 | 		return true;
 37 | 	}
 38 | 
 39 | 	async acquireMigrationLock(): Promise<void> {
 40 | 		// SQLite only has one connection that's reserved by the migration system
 41 | 		// for the whole time between acquireMigrationLock and releaseMigrationLock.
 42 | 		// We don't need to do anything here.
 43 | 	}
 44 | 
 45 | 	async releaseMigrationLock(): Promise<void> {
 46 | 		// SQLite only has one connection that's reserved by the migration system
 47 | 		// for the whole time between acquireMigrationLock and releaseMigrationLock.
 48 | 		// We don't need to do anything here.
 49 | 	}
 50 | 	get supportsOutput(): boolean {
 51 | 		return true;
 52 | 	}
 53 | }
 54 | 
 55 | /**
 56 |  * Config for the SQLite dialect.
 57 |  */
 58 | export interface BunSqliteDialectConfig {
 59 | 	/**
 60 | 	 * An sqlite Database instance or a function that returns one.
 61 | 	 */
 62 | 	database: Database;
 63 | 
 64 | 	/**
 65 | 	 * Called once when the first query is executed.
 66 | 	 */
 67 | 	onCreateConnection?: (connection: DatabaseConnection) => Promise<void>;
 68 | }
 69 | 
 70 | export class BunSqliteDriver implements Driver {
 71 | 	readonly #config: BunSqliteDialectConfig;
 72 | 	readonly #connectionMutex = new ConnectionMutex();
 73 | 
 74 | 	#db?: Database;
 75 | 	#connection?: DatabaseConnection;
 76 | 
 77 | 	constructor(config: BunSqliteDialectConfig) {
 78 | 		this.#config = { ...config };
 79 | 	}
 80 | 
 81 | 	async init(): Promise<void> {
 82 | 		this.#db = this.#config.database;
 83 | 
 84 | 		this.#connection = new BunSqliteConnection(this.#db);
 85 | 
 86 | 		if (this.#config.onCreateConnection) {
 87 | 			await this.#config.onCreateConnection(this.#connection);
 88 | 		}
 89 | 	}
 90 | 
 91 | 	async acquireConnection(): Promise<DatabaseConnection> {
 92 | 		// SQLite only has one single connection. We use a mutex here to wait
 93 | 		// until the single connection has been released.
 94 | 		await this.#connectionMutex.lock();
 95 | 		return this.#connection!;
 96 | 	}
 97 | 
 98 | 	async beginTransaction(connection: DatabaseConnection): Promise<void> {
 99 | 		await connection.executeQuery(CompiledQuery.raw("begin"));
100 | 	}
101 | 
102 | 	async commitTransaction(connection: DatabaseConnection): Promise<void> {
103 | 		await connection.executeQuery(CompiledQuery.raw("commit"));
104 | 	}
105 | 
106 | 	async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
107 | 		await connection.executeQuery(CompiledQuery.raw("rollback"));
108 | 	}
109 | 
110 | 	async releaseConnection(): Promise<void> {
111 | 		this.#connectionMutex.unlock();
112 | 	}
113 | 
114 | 	async destroy(): Promise<void> {
115 | 		this.#db?.close();
116 | 	}
117 | }
118 | 
119 | class BunSqliteConnection implements DatabaseConnection {
120 | 	readonly #db: Database;
121 | 
122 | 	constructor(db: Database) {
123 | 		this.#db = db;
124 | 	}
125 | 
126 | 	executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
127 | 		const { sql, parameters } = compiledQuery;
128 | 		const stmt = this.#db.prepare(sql);
129 | 
130 | 		return Promise.resolve({
131 | 			rows: stmt.all(parameters as any) as O[],
132 | 		});
133 | 	}
134 | 
135 | 	async *streamQuery() {
136 | 		throw new Error("Streaming query is not supported by SQLite driver.");
137 | 	}
138 | }
139 | 
140 | class ConnectionMutex {
141 | 	#promise?: Promise<void>;
142 | 	#resolve?: () => void;
143 | 
144 | 	async lock(): Promise<void> {
145 | 		while (this.#promise) {
146 | 			await this.#promise;
147 | 		}
148 | 
149 | 		this.#promise = new Promise((resolve) => {
150 | 			this.#resolve = resolve;
151 | 		});
152 | 	}
153 | 
154 | 	unlock(): void {
155 | 		const resolve = this.#resolve;
156 | 
157 | 		this.#promise = undefined;
158 | 		this.#resolve = undefined;
159 | 
160 | 		resolve?.();
161 | 	}
162 | }
163 | 
164 | export class BunSqliteIntrospector implements DatabaseIntrospector {
165 | 	readonly #db: Kysely<unknown>;
166 | 
167 | 	constructor(db: Kysely<unknown>) {
168 | 		this.#db = db;
169 | 	}
170 | 
171 | 	async getSchemas(): Promise<SchemaMetadata[]> {
172 | 		// Sqlite doesn't support schemas.
173 | 		return [];
174 | 	}
175 | 
176 | 	async getTables(
177 | 		options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
178 | 	): Promise<TableMetadata[]> {
179 | 		let query = this.#db
180 | 			// @ts-expect-error
181 | 			.selectFrom("sqlite_schema")
182 | 			// @ts-expect-error
183 | 			.where("type", "=", "table")
184 | 			// @ts-expect-error
185 | 			.where("name", "not like", "sqlite_%")
186 | 			.select("name")
187 | 			.$castTo<{ name: string }>();
188 | 
189 | 		if (!options.withInternalKyselyTables) {
190 | 			query = query
191 | 				// @ts-expect-error
192 | 				.where("name", "!=", DEFAULT_MIGRATION_TABLE)
193 | 				// @ts-expect-error
194 | 				.where("name", "!=", DEFAULT_MIGRATION_LOCK_TABLE);
195 | 		}
196 | 
197 | 		const tables = await query.execute();
198 | 		return Promise.all(tables.map(({ name }) => this.#getTableMetadata(name)));
199 | 	}
200 | 
201 | 	async getMetadata(
202 | 		options?: DatabaseMetadataOptions,
203 | 	): Promise<DatabaseMetadata> {
204 | 		return {
205 | 			tables: await this.getTables(options),
206 | 		};
207 | 	}
208 | 
209 | 	async #getTableMetadata(table: string): Promise<TableMetadata> {
210 | 		const db = this.#db;
211 | 
212 | 		// Get the SQL that was used to create the table.
213 | 		const createSql = await db
214 | 			// @ts-expect-error
215 | 			.selectFrom("sqlite_master")
216 | 			// @ts-expect-error
217 | 			.where("name", "=", table)
218 | 			.select("sql")
219 | 			.$castTo<{ sql: string | undefined }>()
220 | 			.execute();
221 | 
222 | 		// Try to find the name of the column that has `autoincrement` 🤦
223 | 		const autoIncrementCol = createSql[0]?.sql
224 | 			?.split(/[\(\),]/)
225 | 			?.find((it) => it.toLowerCase().includes("autoincrement"))
226 | 			?.split(/\s+/)?.[0]
227 | 			?.replace(/["`]/g, "");
228 | 
229 | 		const columns = await db
230 | 			.selectFrom(
231 | 				sql<{
232 | 					name: string;
233 | 					type: string;
234 | 					notnull: 0 | 1;
235 | 					dflt_value: any;
236 | 				}>`pragma_table_info(${table})`.as("table_info"),
237 | 			)
238 | 			.select(["name", "type", "notnull", "dflt_value"])
239 | 			.execute();
240 | 
241 | 		return {
242 | 			name: table,
243 | 			columns: columns.map((col) => ({
244 | 				name: col.name,
245 | 				dataType: col.type,
246 | 				isNullable: !col.notnull,
247 | 				isAutoIncrementing: col.name === autoIncrementCol,
248 | 				hasDefaultValue: col.dflt_value != null,
249 | 			})),
250 | 			isView: true,
251 | 		};
252 | 	}
253 | }
254 | 
255 | export class BunSqliteQueryCompiler extends DefaultQueryCompiler {
256 | 	protected override getCurrentParameterPlaceholder() {
257 | 		return "?";
258 | 	}
259 | 
260 | 	protected override getLeftIdentifierWrapper(): string {
261 | 		return '"';
262 | 	}
263 | 
264 | 	protected override getRightIdentifierWrapper(): string {
265 | 		return '"';
266 | 	}
267 | 
268 | 	protected override getAutoIncrement() {
269 | 		return "autoincrement";
270 | 	}
271 | }
272 | 
273 | export class BunSqliteDialect implements Dialect {
274 | 	readonly #config: BunSqliteDialectConfig;
275 | 
276 | 	constructor(config: BunSqliteDialectConfig) {
277 | 		this.#config = { ...config };
278 | 	}
279 | 
280 | 	createDriver(): Driver {
281 | 		return new BunSqliteDriver(this.#config);
282 | 	}
283 | 
284 | 	createQueryCompiler(): QueryCompiler {
285 | 		return new BunSqliteQueryCompiler();
286 | 	}
287 | 
288 | 	createAdapter(): DialectAdapter {
289 | 		return new BunSqliteAdapter();
290 | 	}
291 | 
292 | 	createIntrospector(db: Kysely<any>): DatabaseIntrospector {
293 | 		return new BunSqliteIntrospector(db);
294 | 	}
295 | }
296 | 
```

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

```typescript
  1 | import { betterFetch } from "@better-fetch/fetch";
  2 | import { generateCodeChallenge, validateAuthorizationCode } from "../oauth2";
  3 | import type { OAuthProvider, ProviderOptions } from "../oauth2";
  4 | 
  5 | export type LoginType =
  6 | 	| 0 /** Facebook OAuth */
  7 | 	| 1 /** Google OAuth */
  8 | 	| 24 /** Apple OAuth */
  9 | 	| 27 /** Microsoft OAuth */
 10 | 	| 97 /** Mobile device */
 11 | 	| 98 /** RingCentral OAuth */
 12 | 	| 99 /** API user */
 13 | 	| 100 /** Zoom Work email */
 14 | 	| 101; /** Single Sign-On (SSO) */
 15 | 
 16 | export type AccountStatus = "pending" | "active" | "inactive";
 17 | 
 18 | export type PronounOption =
 19 | 	| 1 /** Ask the user every time */
 20 | 	| 2 /** Always display */
 21 | 	| 3; /** Do not display */
 22 | 
 23 | export interface PhoneNumber {
 24 | 	/** The country code of the phone number (Example: "+1") */
 25 | 	code: string;
 26 | 
 27 | 	/** The country of the phone number (Example: "US") */
 28 | 	country: string;
 29 | 
 30 | 	/** The label for the phone number (Example: "Mobile") */
 31 | 	label: string;
 32 | 
 33 | 	/** The phone number itself (Example: "800000000") */
 34 | 	number: string;
 35 | 
 36 | 	/** Whether the phone number has been verified (Example: true) */
 37 | 	verified: boolean;
 38 | }
 39 | 
 40 | /**
 41 |  * See the full documentation below:
 42 |  * https://developers.zoom.us/docs/api/users/#tag/users/GET/users/{userId}
 43 |  */
 44 | export interface ZoomProfile extends Record<string, any> {
 45 | 	/** The user's account ID (Example: "q6gBJVO5TzexKYTb_I2rpg") */
 46 | 	account_id: string;
 47 | 	/** The user's account number (Example: 10009239) */
 48 | 	account_number: number;
 49 | 	/** The user's cluster (Example: "us04") */
 50 | 	cluster: string;
 51 | 	/** The user's CMS ID. Only enabled for Kaltura integration (Example: "KDcuGIm1QgePTO8WbOqwIQ") */
 52 | 	cms_user_id: string;
 53 | 	/** The user's cost center (Example: "cost center") */
 54 | 	cost_center: string;
 55 | 	/** User create time (Example: "2018-10-31T04:32:37Z") */
 56 | 	created_at: string;
 57 | 	/** Department (Example: "Developers") */
 58 | 	dept: string;
 59 | 	/** User's display name (Example: "Jill Chill") */
 60 | 	display_name: string;
 61 | 	/** User's email address (Example: "[email protected]") */
 62 | 	email: string;
 63 | 	/** User's first name (Example: "Jill") */
 64 | 	first_name: string;
 65 | 	/** IDs of the web groups that the user belongs to (Example: ["RSMaSp8sTEGK0_oamiA2_w"]) */
 66 | 	group_ids: string[];
 67 | 	/** User ID (Example: "zJKyaiAyTNC-MWjiWC18KQ") */
 68 | 	id: string;
 69 | 	/** IM IDs of the groups that the user belongs to (Example: ["t-_-d56CSWG-7BF15LLrOw"]) */
 70 | 	im_group_ids: string[];
 71 | 	/** The user's JID (Example: "[email protected]") */
 72 | 	jid: string;
 73 | 	/** The user's job title (Example: "API Developer") */
 74 | 	job_title: string;
 75 | 	/** Default language for the Zoom Web Portal (Example: "en-US") */
 76 | 	language: string;
 77 | 	/** User last login client version (Example: "5.9.6.4993(mac)") */
 78 | 	last_client_version: string;
 79 | 	/** User last login time (Example: "2021-05-05T20:40:30Z") */
 80 | 	last_login_time: string;
 81 | 	/** User's last name (Example: "Chill") */
 82 | 	last_name: string;
 83 | 	/** The time zone of the user (Example: "Asia/Shanghai") */
 84 | 	timezone: string;
 85 | 	/** User's location (Example: "Paris") */
 86 | 	location: string;
 87 | 	/** The user's login method (Example: 101) */
 88 | 	login_types: LoginType[];
 89 | 	/** User's personal meeting URL (Example: "example.com") */
 90 | 	personal_meeting_url: string;
 91 | 	/** This field has been deprecated and will not be supported in the future.
 92 | 	 * Use the phone_numbers field instead of this field.
 93 | 	 * The user's phone number (Example: "+1 800000000") */
 94 | 	// @deprecated true
 95 | 	phone_number?: string;
 96 | 	/** The URL for user's profile picture (Example: "example.com") */
 97 | 	pic_url: string;
 98 | 	/** Personal Meeting ID (PMI) (Example: 3542471135) */
 99 | 	pmi: number;
100 | 	/** Unique identifier of the user's assigned role (Example: "0") */
101 | 	role_id: string;
102 | 	/** User's role name (Example: "Admin") */
103 | 	role_name: string;
104 | 	/** Status of user's account (Example: "pending") */
105 | 	status: AccountStatus;
106 | 	/** Use the personal meeting ID (PMI) for instant meetings (Example: false) */
107 | 	use_pmi: boolean;
108 | 	/** The time and date when the user was created (Example: "2018-10-31T04:32:37Z") */
109 | 	user_created_at: string;
110 | 	/** Displays whether user is verified or not (Example: 1) */
111 | 	verified: number;
112 | 	/** The user's Zoom Workplace plan option (Example: 64) */
113 | 	zoom_one_type: number;
114 | 	/** The user's company (Example: "Jill") */
115 | 	company?: string;
116 | 	/** Custom attributes that have been assigned to the user (Example: [{ "key": "cbf_cywdkexrtqc73f97gd4w6g", "name": "A1", "value": "1" }]) */
117 | 	custom_attributes?: { key: string; name: string; value: string }[];
118 | 	/** The employee's unique ID. This field only returns when SAML single sign-on (SSO) is enabled.
119 | 	 * The `login_type` value is `101` (SSO) (Example: "HqDyI037Qjili1kNsSIrIg") */
120 | 	employee_unique_id?: string;
121 | 	/** The manager for the user (Example: "[email protected]") */
122 | 	manager?: string;
123 | 	/** The user's country for the company phone number (Example: "US")
124 | 	 * @deprecated true */
125 | 	phone_country?: string;
126 | 	/** The phone number's ISO country code (Example: "+1") */
127 | 	phone_numbers?: PhoneNumber[];
128 | 	/** The user's plan type (Example: "1") */
129 | 	plan_united_type?: string;
130 | 	/** The user's pronouns (Example: "3123") */
131 | 	pronouns?: string;
132 | 	/** The user's display pronouns setting (Example: 1) */
133 | 	pronouns_option?: PronounOption;
134 | 	/** Personal meeting room URL, if the user has one (Example: "example.com") */
135 | 	vanity_url?: string;
136 | }
137 | 
138 | export interface ZoomOptions extends ProviderOptions<ZoomProfile> {
139 | 	clientId: string;
140 | 	pkce?: boolean;
141 | }
142 | 
143 | export const zoom = (userOptions: ZoomOptions) => {
144 | 	const options = {
145 | 		pkce: true,
146 | 		...userOptions,
147 | 	};
148 | 
149 | 	return {
150 | 		id: "zoom",
151 | 		name: "Zoom",
152 | 		createAuthorizationURL: async ({ state, redirectURI, codeVerifier }) => {
153 | 			const params = new URLSearchParams({
154 | 				response_type: "code",
155 | 				redirect_uri: options.redirectURI ? options.redirectURI : redirectURI,
156 | 				client_id: options.clientId,
157 | 				state,
158 | 			});
159 | 
160 | 			if (options.pkce) {
161 | 				const codeChallenge = await generateCodeChallenge(codeVerifier);
162 | 				params.set("code_challenge_method", "S256");
163 | 				params.set("code_challenge", codeChallenge);
164 | 			}
165 | 
166 | 			const url = new URL("https://zoom.us/oauth/authorize");
167 | 			url.search = params.toString();
168 | 
169 | 			return url;
170 | 		},
171 | 		validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
172 | 			return validateAuthorizationCode({
173 | 				code,
174 | 				redirectURI: options.redirectURI || redirectURI,
175 | 				codeVerifier,
176 | 				options,
177 | 				tokenEndpoint: "https://zoom.us/oauth/token",
178 | 				authentication: "post",
179 | 			});
180 | 		},
181 | 		async getUserInfo(token) {
182 | 			if (options.getUserInfo) {
183 | 				return options.getUserInfo(token);
184 | 			}
185 | 			const { data: profile, error } = await betterFetch<ZoomProfile>(
186 | 				"https://api.zoom.us/v2/users/me",
187 | 				{
188 | 					headers: {
189 | 						authorization: `Bearer ${token.accessToken}`,
190 | 					},
191 | 				},
192 | 			);
193 | 
194 | 			if (error) {
195 | 				return null;
196 | 			}
197 | 
198 | 			const userMap = await options.mapProfileToUser?.(profile);
199 | 
200 | 			return {
201 | 				user: {
202 | 					id: profile.id,
203 | 					name: profile.display_name,
204 | 					image: profile.pic_url,
205 | 					email: profile.email,
206 | 					emailVerified: Boolean(profile.verified),
207 | 					...userMap,
208 | 				},
209 | 				data: {
210 | 					...profile,
211 | 				},
212 | 			};
213 | 		},
214 | 	} satisfies OAuthProvider<ZoomProfile>;
215 | };
216 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/kysely-adapter/node-sqlite-dialect.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @see {@link https://nodejs.org/api/sqlite.html} - Node.js SQLite API documentation
  3 |  */
  4 | import {
  5 | 	Kysely,
  6 | 	CompiledQuery,
  7 | 	DEFAULT_MIGRATION_LOCK_TABLE,
  8 | 	DEFAULT_MIGRATION_TABLE,
  9 | 	sql,
 10 | 	type DatabaseConnection,
 11 | 	type QueryResult,
 12 | 	type DatabaseIntrospector,
 13 | 	type SchemaMetadata,
 14 | 	type DatabaseMetadataOptions,
 15 | 	type TableMetadata,
 16 | 	type DatabaseMetadata,
 17 | 	type Driver,
 18 | 	type Dialect,
 19 | 	type QueryCompiler,
 20 | 	type DialectAdapter,
 21 | } from "kysely";
 22 | import { DefaultQueryCompiler } from "kysely";
 23 | import { DialectAdapterBase } from "kysely";
 24 | import type { DatabaseSync } from "node:sqlite";
 25 | 
 26 | export class NodeSqliteAdapter implements DialectAdapterBase {
 27 | 	get supportsCreateIfNotExists(): boolean {
 28 | 		return true;
 29 | 	}
 30 | 
 31 | 	get supportsTransactionalDdl(): boolean {
 32 | 		return false;
 33 | 	}
 34 | 
 35 | 	get supportsReturning(): boolean {
 36 | 		return true;
 37 | 	}
 38 | 
 39 | 	async acquireMigrationLock(): Promise<void> {
 40 | 		// SQLite only has one connection that's reserved by the migration system
 41 | 		// for the whole time between acquireMigrationLock and releaseMigrationLock.
 42 | 		// We don't need to do anything here.
 43 | 	}
 44 | 
 45 | 	async releaseMigrationLock(): Promise<void> {
 46 | 		// SQLite only has one connection that's reserved by the migration system
 47 | 		// for the whole time between acquireMigrationLock and releaseMigrationLock.
 48 | 		// We don't need to do anything here.
 49 | 	}
 50 | 	get supportsOutput(): boolean {
 51 | 		return true;
 52 | 	}
 53 | }
 54 | 
 55 | /**
 56 |  * Config for the SQLite dialect.
 57 |  */
 58 | export interface NodeSqliteDialectConfig {
 59 | 	/**
 60 | 	 * A sqlite DatabaseSync instance or a function that returns one.
 61 | 	 */
 62 | 	database: DatabaseSync;
 63 | 
 64 | 	/**
 65 | 	 * Called once when the first query is executed.
 66 | 	 */
 67 | 	onCreateConnection?: (connection: DatabaseConnection) => Promise<void>;
 68 | }
 69 | 
 70 | export class NodeSqliteDriver implements Driver {
 71 | 	readonly #config: NodeSqliteDialectConfig;
 72 | 	readonly #connectionMutex = new ConnectionMutex();
 73 | 
 74 | 	#db?: DatabaseSync;
 75 | 	#connection?: DatabaseConnection;
 76 | 
 77 | 	constructor(config: NodeSqliteDialectConfig) {
 78 | 		this.#config = { ...config };
 79 | 	}
 80 | 
 81 | 	async init(): Promise<void> {
 82 | 		this.#db = this.#config.database;
 83 | 
 84 | 		this.#connection = new NodeSqliteConnection(this.#db);
 85 | 
 86 | 		if (this.#config.onCreateConnection) {
 87 | 			await this.#config.onCreateConnection(this.#connection);
 88 | 		}
 89 | 	}
 90 | 
 91 | 	async acquireConnection(): Promise<DatabaseConnection> {
 92 | 		// SQLite only has one single connection. We use a mutex here to wait
 93 | 		// until the single connection has been released.
 94 | 		await this.#connectionMutex.lock();
 95 | 		return this.#connection!;
 96 | 	}
 97 | 
 98 | 	async beginTransaction(connection: DatabaseConnection): Promise<void> {
 99 | 		await connection.executeQuery(CompiledQuery.raw("begin"));
100 | 	}
101 | 
102 | 	async commitTransaction(connection: DatabaseConnection): Promise<void> {
103 | 		await connection.executeQuery(CompiledQuery.raw("commit"));
104 | 	}
105 | 
106 | 	async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
107 | 		await connection.executeQuery(CompiledQuery.raw("rollback"));
108 | 	}
109 | 
110 | 	async releaseConnection(): Promise<void> {
111 | 		this.#connectionMutex.unlock();
112 | 	}
113 | 
114 | 	async destroy(): Promise<void> {
115 | 		this.#db?.close();
116 | 	}
117 | }
118 | 
119 | class NodeSqliteConnection implements DatabaseConnection {
120 | 	readonly #db: DatabaseSync;
121 | 
122 | 	constructor(db: DatabaseSync) {
123 | 		this.#db = db;
124 | 	}
125 | 
126 | 	executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
127 | 		const { sql, parameters } = compiledQuery;
128 | 		const stmt = this.#db.prepare(sql);
129 | 
130 | 		const rows = stmt.all(...(parameters as any[])) as O[];
131 | 
132 | 		return Promise.resolve({
133 | 			rows,
134 | 		});
135 | 	}
136 | 
137 | 	async *streamQuery() {
138 | 		throw new Error("Streaming query is not supported by SQLite driver.");
139 | 	}
140 | }
141 | 
142 | class ConnectionMutex {
143 | 	#promise?: Promise<void>;
144 | 	#resolve?: () => void;
145 | 
146 | 	async lock(): Promise<void> {
147 | 		while (this.#promise) {
148 | 			await this.#promise;
149 | 		}
150 | 
151 | 		this.#promise = new Promise((resolve) => {
152 | 			this.#resolve = resolve;
153 | 		});
154 | 	}
155 | 
156 | 	unlock(): void {
157 | 		const resolve = this.#resolve;
158 | 
159 | 		this.#promise = undefined;
160 | 		this.#resolve = undefined;
161 | 
162 | 		resolve?.();
163 | 	}
164 | }
165 | 
166 | export class NodeSqliteIntrospector implements DatabaseIntrospector {
167 | 	readonly #db: Kysely<unknown>;
168 | 
169 | 	constructor(db: Kysely<unknown>) {
170 | 		this.#db = db;
171 | 	}
172 | 
173 | 	async getSchemas(): Promise<SchemaMetadata[]> {
174 | 		// Sqlite doesn't support schemas.
175 | 		return [];
176 | 	}
177 | 
178 | 	async getTables(
179 | 		options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
180 | 	): Promise<TableMetadata[]> {
181 | 		let query = this.#db
182 | 			// @ts-expect-error
183 | 			.selectFrom("sqlite_schema")
184 | 			// @ts-expect-error
185 | 			.where("type", "=", "table")
186 | 			// @ts-expect-error
187 | 			.where("name", "not like", "sqlite_%")
188 | 			.select("name")
189 | 			.$castTo<{ name: string }>();
190 | 
191 | 		if (!options.withInternalKyselyTables) {
192 | 			query = query
193 | 				// @ts-expect-error
194 | 				.where("name", "!=", DEFAULT_MIGRATION_TABLE)
195 | 				// @ts-expect-error
196 | 				.where("name", "!=", DEFAULT_MIGRATION_LOCK_TABLE);
197 | 		}
198 | 
199 | 		const tables = await query.execute();
200 | 		return Promise.all(tables.map(({ name }) => this.#getTableMetadata(name)));
201 | 	}
202 | 
203 | 	async getMetadata(
204 | 		options?: DatabaseMetadataOptions,
205 | 	): Promise<DatabaseMetadata> {
206 | 		return {
207 | 			tables: await this.getTables(options),
208 | 		};
209 | 	}
210 | 
211 | 	async #getTableMetadata(table: string): Promise<TableMetadata> {
212 | 		const db = this.#db;
213 | 
214 | 		// Get the SQL that was used to create the table.
215 | 		const createSql = await db
216 | 			// @ts-expect-error
217 | 			.selectFrom("sqlite_master")
218 | 			// @ts-expect-error
219 | 			.where("name", "=", table)
220 | 			.select("sql")
221 | 			.$castTo<{ sql: string | undefined }>()
222 | 			.execute();
223 | 
224 | 		// Try to find the name of the column that has `autoincrement` >&
225 | 		const autoIncrementCol = createSql[0]?.sql
226 | 			?.split(/[\(\),]/)
227 | 			?.find((it) => it.toLowerCase().includes("autoincrement"))
228 | 			?.split(/\s+/)?.[0]
229 | 			?.replace(/["`]/g, "");
230 | 
231 | 		const columns = await db
232 | 			.selectFrom(
233 | 				sql<{
234 | 					name: string;
235 | 					type: string;
236 | 					notnull: 0 | 1;
237 | 					dflt_value: any;
238 | 				}>`pragma_table_info(${table})`.as("table_info"),
239 | 			)
240 | 			.select(["name", "type", "notnull", "dflt_value"])
241 | 			.execute();
242 | 
243 | 		return {
244 | 			name: table,
245 | 			columns: columns.map((col) => ({
246 | 				name: col.name,
247 | 				dataType: col.type,
248 | 				isNullable: !col.notnull,
249 | 				isAutoIncrementing: col.name === autoIncrementCol,
250 | 				hasDefaultValue: col.dflt_value != null,
251 | 			})),
252 | 			isView: true,
253 | 		};
254 | 	}
255 | }
256 | 
257 | export class NodeSqliteQueryCompiler extends DefaultQueryCompiler {
258 | 	protected override getCurrentParameterPlaceholder() {
259 | 		return "?";
260 | 	}
261 | 
262 | 	protected override getLeftIdentifierWrapper(): string {
263 | 		return '"';
264 | 	}
265 | 
266 | 	protected override getRightIdentifierWrapper(): string {
267 | 		return '"';
268 | 	}
269 | 
270 | 	protected override getAutoIncrement() {
271 | 		return "autoincrement";
272 | 	}
273 | }
274 | 
275 | export class NodeSqliteDialect implements Dialect {
276 | 	readonly #config: NodeSqliteDialectConfig;
277 | 
278 | 	constructor(config: NodeSqliteDialectConfig) {
279 | 		this.#config = { ...config };
280 | 	}
281 | 
282 | 	createDriver(): Driver {
283 | 		return new NodeSqliteDriver(this.#config);
284 | 	}
285 | 
286 | 	createQueryCompiler(): QueryCompiler {
287 | 		return new NodeSqliteQueryCompiler();
288 | 	}
289 | 
290 | 	createAdapter(): DialectAdapter {
291 | 		return new NodeSqliteAdapter();
292 | 	}
293 | 
294 | 	createIntrospector(db: Kysely<any>): DatabaseIntrospector {
295 | 		return new NodeSqliteIntrospector(db);
296 | 	}
297 | }
298 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/authentication/apple.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Apple
  3 | description: Apple provider setup and usage.
  4 | ---
  5 | <Steps>
  6 |     <Step>
  7 |         ### Get your OAuth credentials
  8 |         To use Apple sign in, you need a client ID and client secret. You can get them from the [Apple Developer Portal](https://developer.apple.com/account/resources/authkeys/list).
  9 | 
 10 |         You will need an active **Apple Developer account** to access the developer portal and generate these credentials.
 11 | 
 12 |         Follow these steps to set up your App ID, Service ID, and generate the key needed for your client secret:
 13 | 
 14 |         1.  **Navigate to Certificates, Identifiers & Profiles:**
 15 |             In the Apple Developer Portal, go to the "Certificates, Identifiers & Profiles" section.
 16 | 
 17 |         2.  **Create an App ID:**
 18 |             *   Go to the `Identifiers` tab.
 19 |             *   Click the `+` icon next to Identifiers.
 20 |             *   Select `App IDs`, then click `Continue`.
 21 |             *   Select `App` as the type, then click `Continue`.
 22 |             *   **Description:** Enter a name for your app (e.g., "My Awesome App"). This name may be displayed to users when they sign in.
 23 |             *   **Bundle ID:** Set a bundle ID. The recommended format is a reverse domain name (e.g., `com.yourcompany.yourapp`). Using a suffix like `.ai` (for app identifier) can help with organization but is not required (e.g., `com.yourcompany.yourapp.ai`).
 24 |             *   Scroll down to **Capabilities**. Select the checkbox for `Sign In with Apple`.
 25 |             *   Click `Continue`, then `Register`.
 26 | 
 27 |         3.  **Create a Service ID:**
 28 |             *   Go back to the `Identifiers` tab.
 29 |             *   Click the `+` icon.
 30 |             *   Select `Service IDs`, then click `Continue`.
 31 |             *   **Description:** Enter a description for this service (e.g., your app name again).
 32 |             *   **Identifier:** Set a unique identifier for the service. Use a reverse domain format, distinct from your App ID (e.g., `com.yourcompany.yourapp.si`, where `.si` indicates service identifier - this is for your organization and not required). **This Service ID will be your `clientId`.**
 33 |             *   Click `Continue`, then `Register`.
 34 | 
 35 |         4.  **Configure the Service ID:**
 36 |             *   Find the Service ID you just created in the `Identifiers` list and click on it.
 37 |             *   Check the `Sign In with Apple` capability, then click `Configure`.
 38 |             *   Under **Primary App ID**, select the App ID you created earlier (e.g., `com.yourcompany.yourapp.ai`).
 39 |             *   Under **Domains and Subdomains**, list all the root domains you will use for Sign In with Apple (e.g., `example.com`, `anotherdomain.com`).
 40 |             *   Under **Return URLs**, enter the callback URL. `https://yourdomain.com/api/auth/callback/apple`. Add all necessary return URLs.
 41 |             *   Click `Next`, then `Done`.
 42 |             *   Click `Continue`, then `Save`.
 43 | 
 44 |         5.  **Create a Client Secret Key:**
 45 |             *   Go to the `Keys` tab.
 46 |             *   Click the `+` icon to create a new key.
 47 |             *   **Key Name:** Enter a name for the key (e.g., "Sign In with Apple Key").
 48 |             *   Scroll down and select the checkbox for `Sign In with Apple`.
 49 |             *   Click the `Configure` button next to `Sign In with Apple`.
 50 |             *   Select the **Primary App ID** you created earlier.
 51 |             *   Click `Save`, then `Continue`, then `Register`.
 52 |             *   **Download the Key:** Immediately download the `.p8` key file. **This file is only available for download once.** Note the Key ID (available on the Keys page after creation) and your Team ID (available in your Apple Developer Account settings).
 53 | 
 54 |         6.  **Generate the Client Secret (JWT):**
 55 |             Apple requires a JSON Web Token (JWT) to be generated dynamically using the downloaded `.p8` key, the Key ID, and your Team ID. This JWT serves as your `clientSecret`.
 56 | 
 57 |             You can use the guide below from [Apple's documentation](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret) to understand how to generate this client secret. You can also use our built in generator [below](#generate-apple-client-secret-jwt) to generate the client secret JWT required for 'Sign in with Apple'.
 58 | 
 59 |             **Note:** Apple allows a maximum expiration of 6 months (180 days) for the client secret JWT. You will need to regenerate the client secret before it expires to maintain uninterrupted authentication.
 60 | 
 61 | 
 62 |     </Step>
 63 |     <Step>
 64 |         ### Configure the provider
 65 |         To configure the provider, you need to add it to the `socialProviders` option of the auth instance.
 66 | 
 67 |         You also need to add `https://appleid.apple.com` to the `trustedOrigins` array in your auth instance configuration to allow communication with Apple's authentication servers.
 68 | 
 69 |         ```ts title="auth.ts"
 70 |         import { betterAuth } from "better-auth"
 71 | 
 72 |         export const auth = betterAuth({
 73 |             socialProviders: {
 74 |                 apple: { // [!code highlight]
 75 |                     clientId: process.env.APPLE_CLIENT_ID as string, // [!code highlight]
 76 |                     clientSecret: process.env.APPLE_CLIENT_SECRET as string, // [!code highlight]
 77 |                     // Optional
 78 |                     appBundleIdentifier: process.env.APPLE_APP_BUNDLE_IDENTIFIER as string, // [!code highlight]
 79 |                 }, // [!code highlight]
 80 |             },
 81 |             // Add appleid.apple.com to trustedOrigins for Sign In with Apple flows
 82 |             trustedOrigins: ["https://appleid.apple.com"], // [!code highlight]
 83 |         })
 84 |         ```
 85 | 
 86 |         On native iOS, it doesn't use the service ID but the app ID (bundle ID) as client ID, so if using the service ID as `clientId` in `signIn.social` with `idToken`, it throws an error: `JWTClaimValidationFailed: unexpected "aud" claim value`. So you need to provide the `appBundleIdentifier` when you want to sign in with Apple using the ID Token.
 87 |     </Step>
 88 | </Steps>
 89 | 
 90 | 
 91 | ## Usage
 92 | 
 93 | ### Sign In with Apple
 94 | 
 95 | To sign in with Apple, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
 96 | - `provider`: The provider to use. It should be set to `apple`.
 97 | 
 98 | ```ts title="auth-client.ts"  /
 99 | import { createAuthClient } from "better-auth/client"
100 | const authClient =  createAuthClient()
101 | 
102 | const signIn = async () => {
103 |     const data = await authClient.signIn.social({
104 |         provider: "apple"
105 |     })
106 | }
107 | ```
108 | 
109 | 
110 | ### Sign In with Apple With ID Token
111 | 
112 | To sign in with Apple using the ID Token, you can use the `signIn.social` function to pass the ID Token. 
113 | 
114 | This is useful when you have the ID Token from Apple on the client-side and want to use it to sign in on the server.
115 | 
116 | <Callout>
117 |  If ID token is provided no redirection will happen, and the user will be signed in directly.
118 | </Callout>
119 | 
120 | ```ts title="auth-client.ts"
121 | await authClient.signIn.social({
122 |     provider: "apple",
123 |     idToken: {
124 |         token: // Apple ID Token,
125 |         nonce: // Nonce (optional)
126 |         accessToken: // Access Token (optional)
127 |     }
128 | })
129 | ```
130 | 
131 | ## Generate Apple Client Secret (JWT)
132 | 
133 | <GenerateAppleJwt />
134 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/db/with-hooks.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { DBPreservedModels } from "@better-auth/core/db";
  2 | import type { BetterAuthOptions } from "@better-auth/core";
  3 | import type { DBAdapter, Where } from "@better-auth/core/db/adapter";
  4 | import { getCurrentAdapter } from "@better-auth/core/context";
  5 | import { getCurrentAuthContext } from "@better-auth/core/context";
  6 | 
  7 | export function getWithHooks(
  8 | 	adapter: DBAdapter<BetterAuthOptions>,
  9 | 	ctx: {
 10 | 		options: BetterAuthOptions;
 11 | 		hooks: Exclude<BetterAuthOptions["databaseHooks"], undefined>[];
 12 | 	},
 13 | ) {
 14 | 	const hooks = ctx.hooks;
 15 | 	type BaseModels = Extract<
 16 | 		DBPreservedModels,
 17 | 		"user" | "account" | "session" | "verification"
 18 | 	>;
 19 | 	async function createWithHooks<T extends Record<string, any>>(
 20 | 		data: T,
 21 | 		model: BaseModels,
 22 | 		customCreateFn?: {
 23 | 			fn: (data: Record<string, any>) => void | Promise<any>;
 24 | 			executeMainFn?: boolean;
 25 | 		},
 26 | 	) {
 27 | 		const context = await getCurrentAuthContext();
 28 | 		let actualData = data;
 29 | 		for (const hook of hooks || []) {
 30 | 			const toRun = hook[model]?.create?.before;
 31 | 			if (toRun) {
 32 | 				// @ts-expect-error context type mismatch
 33 | 				const result = await toRun(actualData as any, context);
 34 | 				if (result === false) {
 35 | 					return null;
 36 | 				}
 37 | 				const isObject = typeof result === "object" && "data" in result;
 38 | 				if (isObject) {
 39 | 					actualData = {
 40 | 						...actualData,
 41 | 						...result.data,
 42 | 					};
 43 | 				}
 44 | 			}
 45 | 		}
 46 | 
 47 | 		const customCreated = customCreateFn
 48 | 			? await customCreateFn.fn(actualData)
 49 | 			: null;
 50 | 		const created =
 51 | 			!customCreateFn || customCreateFn.executeMainFn
 52 | 				? await (await getCurrentAdapter(adapter)).create<T>({
 53 | 						model,
 54 | 						data: actualData as any,
 55 | 						forceAllowId: true,
 56 | 					})
 57 | 				: customCreated;
 58 | 
 59 | 		for (const hook of hooks || []) {
 60 | 			const toRun = hook[model]?.create?.after;
 61 | 			if (toRun) {
 62 | 				// @ts-expect-error context type mismatch
 63 | 				await toRun(created as any, context);
 64 | 			}
 65 | 		}
 66 | 
 67 | 		return created;
 68 | 	}
 69 | 
 70 | 	async function updateWithHooks<T extends Record<string, any>>(
 71 | 		data: any,
 72 | 		where: Where[],
 73 | 		model: BaseModels,
 74 | 		customUpdateFn?: {
 75 | 			fn: (data: Record<string, any>) => void | Promise<any>;
 76 | 			executeMainFn?: boolean;
 77 | 		},
 78 | 	) {
 79 | 		const context = await getCurrentAuthContext();
 80 | 		let actualData = data;
 81 | 
 82 | 		for (const hook of hooks || []) {
 83 | 			const toRun = hook[model]?.update?.before;
 84 | 			if (toRun) {
 85 | 				// @ts-expect-error context type mismatch
 86 | 				const result = await toRun(data as any, context);
 87 | 				if (result === false) {
 88 | 					return null;
 89 | 				}
 90 | 				const isObject = typeof result === "object";
 91 | 				actualData = isObject ? (result as any).data : result;
 92 | 			}
 93 | 		}
 94 | 
 95 | 		const customUpdated = customUpdateFn
 96 | 			? await customUpdateFn.fn(actualData)
 97 | 			: null;
 98 | 
 99 | 		const updated =
100 | 			!customUpdateFn || customUpdateFn.executeMainFn
101 | 				? await (await getCurrentAdapter(adapter)).update<T>({
102 | 						model,
103 | 						update: actualData,
104 | 						where,
105 | 					})
106 | 				: customUpdated;
107 | 
108 | 		for (const hook of hooks || []) {
109 | 			const toRun = hook[model]?.update?.after;
110 | 			if (toRun) {
111 | 				// @ts-expect-error context type mismatch
112 | 				await toRun(updated as any, context);
113 | 			}
114 | 		}
115 | 		return updated;
116 | 	}
117 | 
118 | 	async function updateManyWithHooks<T extends Record<string, any>>(
119 | 		data: any,
120 | 		where: Where[],
121 | 		model: BaseModels,
122 | 		customUpdateFn?: {
123 | 			fn: (data: Record<string, any>) => void | Promise<any>;
124 | 			executeMainFn?: boolean;
125 | 		},
126 | 	) {
127 | 		const context = await getCurrentAuthContext();
128 | 		let actualData = data;
129 | 
130 | 		for (const hook of hooks || []) {
131 | 			const toRun = hook[model]?.update?.before;
132 | 			if (toRun) {
133 | 				// @ts-expect-error context type mismatch
134 | 				const result = await toRun(data as any, context);
135 | 				if (result === false) {
136 | 					return null;
137 | 				}
138 | 				const isObject = typeof result === "object";
139 | 				actualData = isObject ? (result as any).data : result;
140 | 			}
141 | 		}
142 | 
143 | 		const customUpdated = customUpdateFn
144 | 			? await customUpdateFn.fn(actualData)
145 | 			: null;
146 | 
147 | 		const updated =
148 | 			!customUpdateFn || customUpdateFn.executeMainFn
149 | 				? await (await getCurrentAdapter(adapter)).updateMany({
150 | 						model,
151 | 						update: actualData,
152 | 						where,
153 | 					})
154 | 				: customUpdated;
155 | 
156 | 		for (const hook of hooks || []) {
157 | 			const toRun = hook[model]?.update?.after;
158 | 			if (toRun) {
159 | 				// @ts-expect-error context type mismatch
160 | 				await toRun(updated as any, context);
161 | 			}
162 | 		}
163 | 
164 | 		return updated;
165 | 	}
166 | 
167 | 	async function deleteWithHooks<T extends Record<string, any>>(
168 | 		where: Where[],
169 | 		model: BaseModels,
170 | 		customDeleteFn?: {
171 | 			fn: (where: Where[]) => void | Promise<any>;
172 | 			executeMainFn?: boolean;
173 | 		},
174 | 	) {
175 | 		const context = await getCurrentAuthContext();
176 | 		let entityToDelete: T | null = null;
177 | 
178 | 		try {
179 | 			const entities = await (await getCurrentAdapter(adapter)).findMany<T>({
180 | 				model,
181 | 				where,
182 | 				limit: 1,
183 | 			});
184 | 			entityToDelete = entities[0] || null;
185 | 		} catch (error) {
186 | 			// If we can't find the entity, we'll still proceed with deletion
187 | 		}
188 | 
189 | 		if (entityToDelete) {
190 | 			for (const hook of hooks || []) {
191 | 				const toRun = hook[model]?.delete?.before;
192 | 				if (toRun) {
193 | 					// @ts-expect-error context type mismatch
194 | 					const result = await toRun(entityToDelete as any, context);
195 | 					if (result === false) {
196 | 						return null;
197 | 					}
198 | 				}
199 | 			}
200 | 		}
201 | 
202 | 		const customDeleted = customDeleteFn
203 | 			? await customDeleteFn.fn(where)
204 | 			: null;
205 | 
206 | 		const deleted =
207 | 			!customDeleteFn || customDeleteFn.executeMainFn
208 | 				? await (await getCurrentAdapter(adapter)).delete({
209 | 						model,
210 | 						where,
211 | 					})
212 | 				: customDeleted;
213 | 
214 | 		if (entityToDelete) {
215 | 			for (const hook of hooks || []) {
216 | 				const toRun = hook[model]?.delete?.after;
217 | 				if (toRun) {
218 | 					// @ts-expect-error context type mismatch
219 | 					await toRun(entityToDelete as any, context);
220 | 				}
221 | 			}
222 | 		}
223 | 
224 | 		return deleted;
225 | 	}
226 | 
227 | 	async function deleteManyWithHooks<T extends Record<string, any>>(
228 | 		where: Where[],
229 | 		model: BaseModels,
230 | 		customDeleteFn?: {
231 | 			fn: (where: Where[]) => void | Promise<any>;
232 | 			executeMainFn?: boolean;
233 | 		},
234 | 	) {
235 | 		const context = await getCurrentAuthContext();
236 | 		let entitiesToDelete: T[] = [];
237 | 
238 | 		try {
239 | 			entitiesToDelete = await (await getCurrentAdapter(adapter)).findMany<T>({
240 | 				model,
241 | 				where,
242 | 			});
243 | 		} catch (error) {
244 | 			// If we can't find the entities, we'll still proceed with deletion
245 | 		}
246 | 
247 | 		for (const entity of entitiesToDelete) {
248 | 			for (const hook of hooks || []) {
249 | 				const toRun = hook[model]?.delete?.before;
250 | 				if (toRun) {
251 | 					// @ts-expect-error context type mismatch
252 | 					const result = await toRun(entity as any, context);
253 | 					if (result === false) {
254 | 						return null;
255 | 					}
256 | 				}
257 | 			}
258 | 		}
259 | 
260 | 		const customDeleted = customDeleteFn
261 | 			? await customDeleteFn.fn(where)
262 | 			: null;
263 | 
264 | 		const deleted =
265 | 			!customDeleteFn || customDeleteFn.executeMainFn
266 | 				? await (await getCurrentAdapter(adapter)).deleteMany({
267 | 						model,
268 | 						where,
269 | 					})
270 | 				: customDeleted;
271 | 
272 | 		for (const entity of entitiesToDelete) {
273 | 			for (const hook of hooks || []) {
274 | 				const toRun = hook[model]?.delete?.after;
275 | 				if (toRun) {
276 | 					// @ts-expect-error context type mismatch
277 | 					await toRun(entity as any, context);
278 | 				}
279 | 			}
280 | 		}
281 | 
282 | 		return deleted;
283 | 	}
284 | 
285 | 	return {
286 | 		createWithHooks,
287 | 		updateWithHooks,
288 | 		updateManyWithHooks,
289 | 		deleteWithHooks,
290 | 		deleteManyWithHooks,
291 | 	};
292 | }
293 | 
```

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

```typescript
  1 | import * as z from "zod";
  2 | import { createAuthEndpoint } from "@better-auth/core/api";
  3 | import { APIError } from "../../../api";
  4 | import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
  5 | import type { apiKeySchema } from "../schema";
  6 | import type { ApiKey } from "../types";
  7 | import { isRateLimited } from "../rate-limit";
  8 | import type { PredefinedApiKeyOptions } from ".";
  9 | import { safeJSONParse } from "../../../utils/json";
 10 | import { role } from "../../access";
 11 | import { defaultKeyHasher } from "../";
 12 | import type { AuthContext, GenericEndpointContext } from "@better-auth/core";
 13 | 
 14 | export async function validateApiKey({
 15 | 	hashedKey,
 16 | 	ctx,
 17 | 	opts,
 18 | 	schema,
 19 | 	permissions,
 20 | }: {
 21 | 	hashedKey: string;
 22 | 	opts: PredefinedApiKeyOptions;
 23 | 	schema: ReturnType<typeof apiKeySchema>;
 24 | 	permissions?: Record<string, string[]>;
 25 | 	ctx: GenericEndpointContext;
 26 | }) {
 27 | 	const apiKey = await ctx.context.adapter.findOne<ApiKey>({
 28 | 		model: API_KEY_TABLE_NAME,
 29 | 		where: [
 30 | 			{
 31 | 				field: "key",
 32 | 				value: hashedKey,
 33 | 			},
 34 | 		],
 35 | 	});
 36 | 
 37 | 	if (!apiKey) {
 38 | 		throw new APIError("UNAUTHORIZED", {
 39 | 			message: ERROR_CODES.INVALID_API_KEY,
 40 | 		});
 41 | 	}
 42 | 
 43 | 	if (apiKey.enabled === false) {
 44 | 		throw new APIError("UNAUTHORIZED", {
 45 | 			message: ERROR_CODES.KEY_DISABLED,
 46 | 			code: "KEY_DISABLED" as const,
 47 | 		});
 48 | 	}
 49 | 
 50 | 	if (apiKey.expiresAt) {
 51 | 		const now = new Date().getTime();
 52 | 		const expiresAt = new Date(apiKey.expiresAt).getTime();
 53 | 		if (now > expiresAt) {
 54 | 			try {
 55 | 				ctx.context.adapter.delete({
 56 | 					model: API_KEY_TABLE_NAME,
 57 | 					where: [
 58 | 						{
 59 | 							field: "id",
 60 | 							value: apiKey.id,
 61 | 						},
 62 | 					],
 63 | 				});
 64 | 			} catch (error) {
 65 | 				ctx.context.logger.error(`Failed to delete expired API keys:`, error);
 66 | 			}
 67 | 
 68 | 			throw new APIError("UNAUTHORIZED", {
 69 | 				message: ERROR_CODES.KEY_EXPIRED,
 70 | 				code: "KEY_EXPIRED" as const,
 71 | 			});
 72 | 		}
 73 | 	}
 74 | 
 75 | 	if (permissions) {
 76 | 		const apiKeyPermissions = apiKey.permissions
 77 | 			? safeJSONParse<{
 78 | 					[key: string]: string[];
 79 | 				}>(apiKey.permissions)
 80 | 			: null;
 81 | 
 82 | 		if (!apiKeyPermissions) {
 83 | 			throw new APIError("UNAUTHORIZED", {
 84 | 				message: ERROR_CODES.KEY_NOT_FOUND,
 85 | 				code: "KEY_NOT_FOUND" as const,
 86 | 			});
 87 | 		}
 88 | 		const r = role(apiKeyPermissions as any);
 89 | 		const result = r.authorize(permissions);
 90 | 		if (!result.success) {
 91 | 			throw new APIError("UNAUTHORIZED", {
 92 | 				message: ERROR_CODES.KEY_NOT_FOUND,
 93 | 				code: "KEY_NOT_FOUND" as const,
 94 | 			});
 95 | 		}
 96 | 	}
 97 | 
 98 | 	let remaining = apiKey.remaining;
 99 | 	let lastRefillAt = apiKey.lastRefillAt;
100 | 
101 | 	if (apiKey.remaining === 0 && apiKey.refillAmount === null) {
102 | 		// if there is no more remaining requests, and there is no refill amount, than the key is revoked
103 | 		try {
104 | 			ctx.context.adapter.delete({
105 | 				model: API_KEY_TABLE_NAME,
106 | 				where: [
107 | 					{
108 | 						field: "id",
109 | 						value: apiKey.id,
110 | 					},
111 | 				],
112 | 			});
113 | 		} catch (error) {
114 | 			ctx.context.logger.error(`Failed to delete expired API keys:`, error);
115 | 		}
116 | 
117 | 		throw new APIError("TOO_MANY_REQUESTS", {
118 | 			message: ERROR_CODES.USAGE_EXCEEDED,
119 | 			code: "USAGE_EXCEEDED" as const,
120 | 		});
121 | 	} else if (remaining !== null) {
122 | 		let now = new Date().getTime();
123 | 		const refillInterval = apiKey.refillInterval;
124 | 		const refillAmount = apiKey.refillAmount;
125 | 		let lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();
126 | 
127 | 		if (refillInterval && refillAmount) {
128 | 			// if they provide refill info, then we should refill once the interval is reached.
129 | 
130 | 			const timeSinceLastRequest = now - lastTime;
131 | 			if (timeSinceLastRequest > refillInterval) {
132 | 				remaining = refillAmount;
133 | 				lastRefillAt = new Date();
134 | 			}
135 | 		}
136 | 
137 | 		if (remaining === 0) {
138 | 			// if there are no more remaining requests, than the key is invalid
139 | 			throw new APIError("TOO_MANY_REQUESTS", {
140 | 				message: ERROR_CODES.USAGE_EXCEEDED,
141 | 				code: "USAGE_EXCEEDED" as const,
142 | 			});
143 | 		} else {
144 | 			remaining--;
145 | 		}
146 | 	}
147 | 
148 | 	const { message, success, update, tryAgainIn } = isRateLimited(apiKey, opts);
149 | 
150 | 	const newApiKey = await ctx.context.adapter.update<ApiKey>({
151 | 		model: API_KEY_TABLE_NAME,
152 | 		where: [
153 | 			{
154 | 				field: "id",
155 | 				value: apiKey.id,
156 | 			},
157 | 		],
158 | 		update: {
159 | 			...update,
160 | 			remaining,
161 | 			lastRefillAt,
162 | 		},
163 | 	});
164 | 
165 | 	if (!newApiKey) {
166 | 		throw new APIError("INTERNAL_SERVER_ERROR", {
167 | 			message: ERROR_CODES.FAILED_TO_UPDATE_API_KEY,
168 | 			code: "INTERNAL_SERVER_ERROR" as const,
169 | 		});
170 | 	}
171 | 
172 | 	if (success === false) {
173 | 		throw new APIError("UNAUTHORIZED", {
174 | 			message: message ?? undefined,
175 | 			code: "RATE_LIMITED" as const,
176 | 			details: {
177 | 				tryAgainIn,
178 | 			},
179 | 		});
180 | 	}
181 | 
182 | 	return newApiKey;
183 | }
184 | 
185 | export function verifyApiKey({
186 | 	opts,
187 | 	schema,
188 | 	deleteAllExpiredApiKeys,
189 | }: {
190 | 	opts: PredefinedApiKeyOptions;
191 | 	schema: ReturnType<typeof apiKeySchema>;
192 | 	deleteAllExpiredApiKeys(
193 | 		ctx: AuthContext,
194 | 		byPassLastCheckTime?: boolean,
195 | 	): void;
196 | }) {
197 | 	return createAuthEndpoint(
198 | 		"/api-key/verify",
199 | 		{
200 | 			method: "POST",
201 | 			body: z.object({
202 | 				key: z.string().meta({
203 | 					description: "The key to verify",
204 | 				}),
205 | 				permissions: z
206 | 					.record(z.string(), z.array(z.string()))
207 | 					.meta({
208 | 						description: "The permissions to verify.",
209 | 					})
210 | 					.optional(),
211 | 			}),
212 | 			metadata: {
213 | 				SERVER_ONLY: true,
214 | 			},
215 | 		},
216 | 		async (ctx) => {
217 | 			const { key } = ctx.body;
218 | 
219 | 			if (key.length < opts.defaultKeyLength) {
220 | 				// if the key is shorter than the default key length, than we know the key is invalid.
221 | 				// we can't check if the key is exactly equal to the default key length, because
222 | 				// a prefix may be added to the key.
223 | 				return ctx.json({
224 | 					valid: false,
225 | 					error: {
226 | 						message: ERROR_CODES.INVALID_API_KEY,
227 | 						code: "KEY_NOT_FOUND" as const,
228 | 					},
229 | 					key: null,
230 | 				});
231 | 			}
232 | 
233 | 			if (opts.customAPIKeyValidator) {
234 | 				const isValid = await opts.customAPIKeyValidator({ ctx, key });
235 | 				if (!isValid) {
236 | 					return ctx.json({
237 | 						valid: false,
238 | 						error: {
239 | 							message: ERROR_CODES.INVALID_API_KEY,
240 | 							code: "KEY_NOT_FOUND" as const,
241 | 						},
242 | 						key: null,
243 | 					});
244 | 				}
245 | 			}
246 | 
247 | 			const hashed = opts.disableKeyHashing ? key : await defaultKeyHasher(key);
248 | 
249 | 			let apiKey: ApiKey | null = null;
250 | 
251 | 			try {
252 | 				apiKey = await validateApiKey({
253 | 					hashedKey: hashed,
254 | 					permissions: ctx.body.permissions,
255 | 					ctx,
256 | 					opts,
257 | 					schema,
258 | 				});
259 | 				await deleteAllExpiredApiKeys(ctx.context);
260 | 			} catch (error) {
261 | 				if (error instanceof APIError) {
262 | 					return ctx.json({
263 | 						valid: false,
264 | 						error: {
265 | 							message: error.body?.message,
266 | 							code: error.body?.code as string,
267 | 						},
268 | 						key: null,
269 | 					});
270 | 				}
271 | 
272 | 				return ctx.json({
273 | 					valid: false,
274 | 					error: {
275 | 						message: ERROR_CODES.INVALID_API_KEY,
276 | 						code: "INVALID_API_KEY" as const,
277 | 					},
278 | 					key: null,
279 | 				});
280 | 			}
281 | 
282 | 			const { key: _, ...returningApiKey } = apiKey ?? {
283 | 				key: 1,
284 | 				permissions: undefined,
285 | 			};
286 | 			if ("metadata" in returningApiKey) {
287 | 				returningApiKey.metadata =
288 | 					schema.apikey.fields.metadata.transform.output(
289 | 						returningApiKey.metadata as never as string,
290 | 					);
291 | 			}
292 | 
293 | 			returningApiKey.permissions = returningApiKey.permissions
294 | 				? safeJSONParse<{
295 | 						[key: string]: string[];
296 | 					}>(returningApiKey.permissions)
297 | 				: null;
298 | 
299 | 			return ctx.json({
300 | 				valid: true,
301 | 				error: null,
302 | 				key: apiKey === null ? null : (returningApiKey as Omit<ApiKey, "key">),
303 | 			});
304 | 		},
305 | 	);
306 | }
307 | 
```

--------------------------------------------------------------------------------
/packages/stripe/src/hooks.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { type GenericEndpointContext, logger } from "better-auth";
  2 | import type Stripe from "stripe";
  3 | import type { InputSubscription, StripeOptions, Subscription } from "./types";
  4 | import { getPlanByPriceInfo } from "./utils";
  5 | 
  6 | export async function onCheckoutSessionCompleted(
  7 | 	ctx: GenericEndpointContext,
  8 | 	options: StripeOptions,
  9 | 	event: Stripe.Event,
 10 | ) {
 11 | 	try {
 12 | 		const client = options.stripeClient;
 13 | 		const checkoutSession = event.data.object as Stripe.Checkout.Session;
 14 | 		if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
 15 | 			return;
 16 | 		}
 17 | 		const subscription = await client.subscriptions.retrieve(
 18 | 			checkoutSession.subscription as string,
 19 | 		);
 20 | 		const priceId = subscription.items.data[0]?.price.id;
 21 | 		const priceLookupKey = subscription.items.data[0]?.price.lookup_key || null;
 22 | 		const plan = await getPlanByPriceInfo(
 23 | 			options,
 24 | 			priceId as string,
 25 | 			priceLookupKey,
 26 | 		);
 27 | 		if (plan) {
 28 | 			const referenceId =
 29 | 				checkoutSession?.client_reference_id ||
 30 | 				checkoutSession?.metadata?.referenceId;
 31 | 			const subscriptionId = checkoutSession?.metadata?.subscriptionId;
 32 | 			const seats = subscription.items.data[0]!.quantity;
 33 | 			if (referenceId && subscriptionId) {
 34 | 				const trial =
 35 | 					subscription.trial_start && subscription.trial_end
 36 | 						? {
 37 | 								trialStart: new Date(subscription.trial_start * 1000),
 38 | 								trialEnd: new Date(subscription.trial_end * 1000),
 39 | 							}
 40 | 						: {};
 41 | 
 42 | 				let dbSubscription =
 43 | 					await ctx.context.adapter.update<InputSubscription>({
 44 | 						model: "subscription",
 45 | 						update: {
 46 | 							plan: plan.name.toLowerCase(),
 47 | 							status: subscription.status,
 48 | 							updatedAt: new Date(),
 49 | 							periodStart: new Date(
 50 | 								subscription.items.data[0]!.current_period_start * 1000,
 51 | 							),
 52 | 							periodEnd: new Date(
 53 | 								subscription.items.data[0]!.current_period_end * 1000,
 54 | 							),
 55 | 							stripeSubscriptionId: checkoutSession.subscription as string,
 56 | 							seats,
 57 | 							...trial,
 58 | 						},
 59 | 						where: [
 60 | 							{
 61 | 								field: "id",
 62 | 								value: subscriptionId,
 63 | 							},
 64 | 						],
 65 | 					});
 66 | 
 67 | 				if (trial.trialStart && plan.freeTrial?.onTrialStart) {
 68 | 					await plan.freeTrial.onTrialStart(dbSubscription as Subscription);
 69 | 				}
 70 | 
 71 | 				if (!dbSubscription) {
 72 | 					dbSubscription = await ctx.context.adapter.findOne<Subscription>({
 73 | 						model: "subscription",
 74 | 						where: [
 75 | 							{
 76 | 								field: "id",
 77 | 								value: subscriptionId,
 78 | 							},
 79 | 						],
 80 | 					});
 81 | 				}
 82 | 				await options.subscription?.onSubscriptionComplete?.(
 83 | 					{
 84 | 						event,
 85 | 						subscription: dbSubscription as Subscription,
 86 | 						stripeSubscription: subscription,
 87 | 						plan,
 88 | 					},
 89 | 					ctx,
 90 | 				);
 91 | 				return;
 92 | 			}
 93 | 		}
 94 | 	} catch (e: any) {
 95 | 		logger.error(`Stripe webhook failed. Error: ${e.message}`);
 96 | 	}
 97 | }
 98 | 
 99 | export async function onSubscriptionUpdated(
100 | 	ctx: GenericEndpointContext,
101 | 	options: StripeOptions,
102 | 	event: Stripe.Event,
103 | ) {
104 | 	try {
105 | 		if (!options.subscription?.enabled) {
106 | 			return;
107 | 		}
108 | 		const subscriptionUpdated = event.data.object as Stripe.Subscription;
109 | 		const priceId = subscriptionUpdated.items.data[0]!.price.id;
110 | 		const priceLookupKey =
111 | 			subscriptionUpdated.items.data[0]!.price.lookup_key || null;
112 | 		const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
113 | 
114 | 		const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
115 | 		const customerId = subscriptionUpdated.customer?.toString();
116 | 		let subscription = await ctx.context.adapter.findOne<Subscription>({
117 | 			model: "subscription",
118 | 			where: subscriptionId
119 | 				? [{ field: "id", value: subscriptionId }]
120 | 				: [{ field: "stripeSubscriptionId", value: subscriptionUpdated.id }],
121 | 		});
122 | 		if (!subscription) {
123 | 			const subs = await ctx.context.adapter.findMany<Subscription>({
124 | 				model: "subscription",
125 | 				where: [{ field: "stripeCustomerId", value: customerId }],
126 | 			});
127 | 			if (subs.length > 1) {
128 | 				const activeSub = subs.find(
129 | 					(sub: Subscription) =>
130 | 						sub.status === "active" || sub.status === "trialing",
131 | 				);
132 | 				if (!activeSub) {
133 | 					logger.warn(
134 | 						`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`,
135 | 					);
136 | 					return;
137 | 				}
138 | 				subscription = activeSub;
139 | 			} else {
140 | 				subscription = subs[0]!;
141 | 			}
142 | 		}
143 | 
144 | 		const seats = subscriptionUpdated.items.data[0]!.quantity;
145 | 		await ctx.context.adapter.update({
146 | 			model: "subscription",
147 | 			update: {
148 | 				...(plan
149 | 					? {
150 | 							plan: plan.name.toLowerCase(),
151 | 							limits: plan.limits,
152 | 						}
153 | 					: {}),
154 | 				updatedAt: new Date(),
155 | 				status: subscriptionUpdated.status,
156 | 				periodStart: new Date(
157 | 					subscriptionUpdated.items.data[0]!.current_period_start * 1000,
158 | 				),
159 | 				periodEnd: new Date(
160 | 					subscriptionUpdated.items.data[0]!.current_period_end * 1000,
161 | 				),
162 | 				cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
163 | 				seats,
164 | 				stripeSubscriptionId: subscriptionUpdated.id,
165 | 			},
166 | 			where: [
167 | 				{
168 | 					field: "id",
169 | 					value: subscription.id,
170 | 				},
171 | 			],
172 | 		});
173 | 		const subscriptionCanceled =
174 | 			subscriptionUpdated.status === "active" &&
175 | 			subscriptionUpdated.cancel_at_period_end &&
176 | 			!subscription.cancelAtPeriodEnd; //if this is true, it means the subscription was canceled before the event was triggered
177 | 		if (subscriptionCanceled) {
178 | 			await options.subscription.onSubscriptionCancel?.({
179 | 				subscription,
180 | 				cancellationDetails:
181 | 					subscriptionUpdated.cancellation_details || undefined,
182 | 				stripeSubscription: subscriptionUpdated,
183 | 				event,
184 | 			});
185 | 		}
186 | 		await options.subscription.onSubscriptionUpdate?.({
187 | 			event,
188 | 			subscription,
189 | 		});
190 | 		if (plan) {
191 | 			if (
192 | 				subscriptionUpdated.status === "active" &&
193 | 				subscription.status === "trialing" &&
194 | 				plan.freeTrial?.onTrialEnd
195 | 			) {
196 | 				await plan.freeTrial.onTrialEnd({ subscription }, ctx);
197 | 			}
198 | 			if (
199 | 				subscriptionUpdated.status === "incomplete_expired" &&
200 | 				subscription.status === "trialing" &&
201 | 				plan.freeTrial?.onTrialExpired
202 | 			) {
203 | 				await plan.freeTrial.onTrialExpired(subscription, ctx);
204 | 			}
205 | 		}
206 | 	} catch (error: any) {
207 | 		logger.error(`Stripe webhook failed. Error: ${error}`);
208 | 	}
209 | }
210 | 
211 | export async function onSubscriptionDeleted(
212 | 	ctx: GenericEndpointContext,
213 | 	options: StripeOptions,
214 | 	event: Stripe.Event,
215 | ) {
216 | 	if (!options.subscription?.enabled) {
217 | 		return;
218 | 	}
219 | 	try {
220 | 		const subscriptionDeleted = event.data.object as Stripe.Subscription;
221 | 		const subscriptionId = subscriptionDeleted.id;
222 | 		const subscription = await ctx.context.adapter.findOne<Subscription>({
223 | 			model: "subscription",
224 | 			where: [
225 | 				{
226 | 					field: "stripeSubscriptionId",
227 | 					value: subscriptionId,
228 | 				},
229 | 			],
230 | 		});
231 | 		if (subscription) {
232 | 			await ctx.context.adapter.update({
233 | 				model: "subscription",
234 | 				where: [
235 | 					{
236 | 						field: "id",
237 | 						value: subscription.id,
238 | 					},
239 | 				],
240 | 				update: {
241 | 					status: "canceled",
242 | 					updatedAt: new Date(),
243 | 				},
244 | 			});
245 | 			await options.subscription.onSubscriptionDeleted?.({
246 | 				event,
247 | 				stripeSubscription: subscriptionDeleted,
248 | 				subscription,
249 | 			});
250 | 		} else {
251 | 			logger.warn(
252 | 				`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
253 | 			);
254 | 		}
255 | 	} catch (error: any) {
256 | 		logger.error(`Stripe webhook failed. Error: ${error}`);
257 | 	}
258 | }
259 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/organization/client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { atom } from "nanostores";
  2 | import type {
  3 | 	InferInvitation,
  4 | 	InferMember,
  5 | 	Invitation,
  6 | 	Member,
  7 | 	Organization,
  8 | 	Team,
  9 | } from "../../plugins/organization/schema";
 10 | import type { Prettify } from "../../types/helper";
 11 | import { type AccessControl, type Role } from "../access";
 12 | import type { BetterAuthClientPlugin } from "@better-auth/core";
 13 | import { type OrganizationPlugin } from "./organization";
 14 | import { useAuthQuery } from "../../client";
 15 | import {
 16 | 	defaultStatements,
 17 | 	adminAc,
 18 | 	memberAc,
 19 | 	ownerAc,
 20 | 	defaultRoles,
 21 | } from "./access";
 22 | import type { DBFieldAttribute } from "@better-auth/core/db";
 23 | import type { BetterAuthOptions, BetterAuthPlugin } from "../../types";
 24 | import type { OrganizationOptions } from "./types";
 25 | import type { HasPermissionBaseInput } from "./permission";
 26 | import { hasPermissionFn } from "./permission";
 27 | 
 28 | /**
 29 |  * Using the same `hasPermissionFn` function, but without the need for a `ctx` parameter or the `organizationId` parameter.
 30 |  */
 31 | export const clientSideHasPermission = (input: HasPermissionBaseInput) => {
 32 | 	const acRoles: {
 33 | 		[x: string]: Role<any> | undefined;
 34 | 	} = input.options.roles || defaultRoles;
 35 | 
 36 | 	return hasPermissionFn(input, acRoles);
 37 | };
 38 | 
 39 | interface OrganizationClientOptions {
 40 | 	ac?: AccessControl;
 41 | 	roles?: {
 42 | 		[key in string]: Role;
 43 | 	};
 44 | 	teams?: {
 45 | 		enabled: boolean;
 46 | 	};
 47 | 	schema?: {
 48 | 		organization?: {
 49 | 			additionalFields?: {
 50 | 				[key: string]: DBFieldAttribute;
 51 | 			};
 52 | 		};
 53 | 		member?: {
 54 | 			additionalFields?: {
 55 | 				[key: string]: DBFieldAttribute;
 56 | 			};
 57 | 		};
 58 | 		invitation?: {
 59 | 			additionalFields?: {
 60 | 				[key: string]: DBFieldAttribute;
 61 | 			};
 62 | 		};
 63 | 		team?: {
 64 | 			additionalFields?: {
 65 | 				[key: string]: DBFieldAttribute;
 66 | 			};
 67 | 		};
 68 | 		organizationRole?: {
 69 | 			additionalFields?: {
 70 | 				[key: string]: DBFieldAttribute;
 71 | 			};
 72 | 		};
 73 | 	};
 74 | 	dynamicAccessControl?: {
 75 | 		enabled: boolean;
 76 | 	};
 77 | }
 78 | 
 79 | export const organizationClient = <CO extends OrganizationClientOptions>(
 80 | 	options?: CO,
 81 | ) => {
 82 | 	const $listOrg = atom<boolean>(false);
 83 | 	const $activeOrgSignal = atom<boolean>(false);
 84 | 	const $activeMemberSignal = atom<boolean>(false);
 85 | 	const $activeMemberRoleSignal = atom<boolean>(false);
 86 | 
 87 | 	type DefaultStatements = typeof defaultStatements;
 88 | 	type Statements = CO["ac"] extends AccessControl<infer S>
 89 | 		? S
 90 | 		: DefaultStatements;
 91 | 	type PermissionType = {
 92 | 		[key in keyof Statements]?: Array<
 93 | 			Statements[key] extends readonly unknown[]
 94 | 				? Statements[key][number]
 95 | 				: never
 96 | 		>;
 97 | 	};
 98 | 	type PermissionExclusive =
 99 | 		| {
100 | 				/**
101 | 				 * @deprecated Use `permissions` instead
102 | 				 */
103 | 				permission: PermissionType;
104 | 				permissions?: never;
105 | 		  }
106 | 		| {
107 | 				permissions: PermissionType;
108 | 				permission?: never;
109 | 		  };
110 | 
111 | 	const roles = {
112 | 		admin: adminAc,
113 | 		member: memberAc,
114 | 		owner: ownerAc,
115 | 		...options?.roles,
116 | 	};
117 | 
118 | 	type OrganizationReturn = CO["teams"] extends { enabled: true }
119 | 		? {
120 | 				members: InferMember<CO>[];
121 | 				invitations: InferInvitation<CO>[];
122 | 				teams: Team[];
123 | 			} & Organization
124 | 		: {
125 | 				members: InferMember<CO>[];
126 | 				invitations: InferInvitation<CO>[];
127 | 			} & Organization;
128 | 
129 | 	type Schema = CO["schema"];
130 | 	return {
131 | 		id: "organization",
132 | 		$InferServerPlugin: {} as OrganizationPlugin<{
133 | 			ac: CO["ac"] extends AccessControl
134 | 				? CO["ac"]
135 | 				: AccessControl<DefaultStatements>;
136 | 			roles: CO["roles"] extends Record<string, Role>
137 | 				? CO["roles"]
138 | 				: {
139 | 						admin: Role;
140 | 						member: Role;
141 | 						owner: Role;
142 | 					};
143 | 			teams: {
144 | 				enabled: CO["teams"] extends { enabled: true } ? true : false;
145 | 			};
146 | 			schema: Schema;
147 | 			dynamicAccessControl: {
148 | 				enabled: CO["dynamicAccessControl"] extends { enabled: true }
149 | 					? true
150 | 					: false;
151 | 			};
152 | 		}>,
153 | 		getActions: ($fetch, _$store, co) => ({
154 | 			$Infer: {
155 | 				ActiveOrganization: {} as OrganizationReturn,
156 | 				Organization: {} as Organization,
157 | 				Invitation: {} as InferInvitation<CO>,
158 | 				Member: {} as InferMember<CO>,
159 | 				Team: {} as Team,
160 | 			},
161 | 			organization: {
162 | 				checkRolePermission: <
163 | 					R extends CO extends { roles: any }
164 | 						? keyof CO["roles"]
165 | 						: "admin" | "member" | "owner",
166 | 				>(
167 | 					data: PermissionExclusive & {
168 | 						role: R;
169 | 					},
170 | 				) => {
171 | 					const isAuthorized = clientSideHasPermission({
172 | 						role: data.role as string,
173 | 						options: {
174 | 							ac: options?.ac,
175 | 							roles: roles,
176 | 						},
177 | 						permissions: (data.permissions ?? data.permission) as any,
178 | 					});
179 | 					return isAuthorized;
180 | 				},
181 | 			},
182 | 		}),
183 | 		getAtoms: ($fetch) => {
184 | 			const listOrganizations = useAuthQuery<Organization[]>(
185 | 				$listOrg,
186 | 				"/organization/list",
187 | 				$fetch,
188 | 				{
189 | 					method: "GET",
190 | 				},
191 | 			);
192 | 			const activeOrganization = useAuthQuery<
193 | 				Prettify<
194 | 					Organization & {
195 | 						members: (Member & {
196 | 							user: {
197 | 								id: string;
198 | 								name: string;
199 | 								email: string;
200 | 								image: string | undefined;
201 | 							};
202 | 						})[];
203 | 						invitations: Invitation[];
204 | 					}
205 | 				>
206 | 			>(
207 | 				[$activeOrgSignal],
208 | 				"/organization/get-full-organization",
209 | 				$fetch,
210 | 				() => ({
211 | 					method: "GET",
212 | 				}),
213 | 			);
214 | 
215 | 			const activeMember = useAuthQuery<Member>(
216 | 				[$activeMemberSignal],
217 | 				"/organization/get-active-member",
218 | 				$fetch,
219 | 				{
220 | 					method: "GET",
221 | 				},
222 | 			);
223 | 
224 | 			const activeMemberRole = useAuthQuery<{ role: string }>(
225 | 				[$activeMemberRoleSignal],
226 | 				"/organization/get-active-member-role",
227 | 				$fetch,
228 | 				{
229 | 					method: "GET",
230 | 				},
231 | 			);
232 | 
233 | 			return {
234 | 				$listOrg,
235 | 				$activeOrgSignal,
236 | 				$activeMemberSignal,
237 | 				$activeMemberRoleSignal,
238 | 				activeOrganization,
239 | 				listOrganizations,
240 | 				activeMember,
241 | 				activeMemberRole,
242 | 			};
243 | 		},
244 | 		pathMethods: {
245 | 			"/organization/get-full-organization": "GET",
246 | 			"/organization/list-user-teams": "GET",
247 | 		},
248 | 		atomListeners: [
249 | 			{
250 | 				matcher(path) {
251 | 					return (
252 | 						path === "/organization/create" ||
253 | 						path === "/organization/delete" ||
254 | 						path === "/organization/update"
255 | 					);
256 | 				},
257 | 				signal: "$listOrg",
258 | 			},
259 | 			{
260 | 				matcher(path) {
261 | 					return path.startsWith("/organization");
262 | 				},
263 | 				signal: "$activeOrgSignal",
264 | 			},
265 | 			{
266 | 				matcher(path) {
267 | 					return path.startsWith("/organization/set-active");
268 | 				},
269 | 				signal: "$sessionSignal",
270 | 			},
271 | 			{
272 | 				matcher(path) {
273 | 					return path.includes("/organization/update-member-role");
274 | 				},
275 | 				signal: "$activeMemberSignal",
276 | 			},
277 | 			{
278 | 				matcher(path) {
279 | 					return path.includes("/organization/update-member-role");
280 | 				},
281 | 				signal: "$activeMemberRoleSignal",
282 | 			},
283 | 		],
284 | 	} satisfies BetterAuthClientPlugin;
285 | };
286 | 
287 | export const inferOrgAdditionalFields = <
288 | 	O extends {
289 | 		options: BetterAuthOptions;
290 | 	},
291 | 	S extends OrganizationOptions["schema"] = undefined,
292 | >(
293 | 	schema?: S,
294 | ) => {
295 | 	type FindById<
296 | 		T extends readonly BetterAuthPlugin[],
297 | 		TargetId extends string,
298 | 	> = Extract<T[number], { id: TargetId }>;
299 | 
300 | 	type Auth = O extends { options: any } ? O : { options: { plugins: [] } };
301 | 
302 | 	type OrganizationPlugin = FindById<
303 | 		// @ts-expect-error
304 | 		Auth["options"]["plugins"],
305 | 		"organization"
306 | 	>;
307 | 	type Schema = O extends Object
308 | 		? O extends Exclude<OrganizationOptions["schema"], undefined>
309 | 			? O
310 | 			: OrganizationPlugin extends { options: { schema: infer S } }
311 | 				? S extends OrganizationOptions["schema"]
312 | 					? S
313 | 					: undefined
314 | 				: undefined
315 | 		: undefined;
316 | 	return {} as undefined extends S ? Schema : S;
317 | };
318 | 
```

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

```typescript
  1 | import type { BetterAuthPlugin } from "@better-auth/core";
  2 | import { schema } from "./schema";
  3 | import { getJwksAdapter } from "./adapter";
  4 | import { getJwtToken, signJWT } from "./sign";
  5 | import type { JSONWebKeySet, JWTPayload } from "jose";
  6 | import { APIError, sessionMiddleware } from "../../api";
  7 | import {
  8 | 	createAuthEndpoint,
  9 | 	createAuthMiddleware,
 10 | } from "@better-auth/core/api";
 11 | import { mergeSchema } from "../../db/schema";
 12 | import * as z from "zod";
 13 | import { BetterAuthError } from "@better-auth/core/error";
 14 | import type { JwtOptions } from "./types";
 15 | import { createJwk } from "./utils";
 16 | export type * from "./types";
 17 | export { generateExportedKeyPair, createJwk } from "./utils";
 18 | 
 19 | export const jwt = (options?: JwtOptions) => {
 20 | 	// Remote url must be set when using signing function
 21 | 	if (options?.jwt?.sign && !options.jwks?.remoteUrl) {
 22 | 		throw new BetterAuthError(
 23 | 			"jwks_config",
 24 | 			"jwks.remoteUrl must be set when using jwt.sign",
 25 | 		);
 26 | 	}
 27 | 
 28 | 	// Alg is required to be specified when using remote url (needed in openid metadata)
 29 | 	if (options?.jwks?.remoteUrl && !options.jwks?.keyPairConfig?.alg) {
 30 | 		throw new BetterAuthError(
 31 | 			"jwks_config",
 32 | 			"must specify alg when using the oidc plugin and jwks.remoteUrl",
 33 | 		);
 34 | 	}
 35 | 
 36 | 	return {
 37 | 		id: "jwt",
 38 | 		options,
 39 | 		endpoints: {
 40 | 			getJwks: createAuthEndpoint(
 41 | 				"/jwks",
 42 | 				{
 43 | 					method: "GET",
 44 | 					metadata: {
 45 | 						openapi: {
 46 | 							description: "Get the JSON Web Key Set",
 47 | 							responses: {
 48 | 								"200": {
 49 | 									description: "JSON Web Key Set retrieved successfully",
 50 | 									content: {
 51 | 										"application/json": {
 52 | 											schema: {
 53 | 												type: "object",
 54 | 												properties: {
 55 | 													keys: {
 56 | 														type: "array",
 57 | 														description: "Array of public JSON Web Keys",
 58 | 														items: {
 59 | 															type: "object",
 60 | 															properties: {
 61 | 																kid: {
 62 | 																	type: "string",
 63 | 																	description:
 64 | 																		"Key ID uniquely identifying the key, corresponds to the 'id' from the stored Jwk",
 65 | 																},
 66 | 																kty: {
 67 | 																	type: "string",
 68 | 																	description:
 69 | 																		"Key type (e.g., 'RSA', 'EC', 'OKP')",
 70 | 																},
 71 | 																alg: {
 72 | 																	type: "string",
 73 | 																	description:
 74 | 																		"Algorithm intended for use with the key (e.g., 'EdDSA', 'RS256')",
 75 | 																},
 76 | 																use: {
 77 | 																	type: "string",
 78 | 																	description:
 79 | 																		"Intended use of the public key (e.g., 'sig' for signature)",
 80 | 																	enum: ["sig"],
 81 | 																	nullable: true,
 82 | 																},
 83 | 																n: {
 84 | 																	type: "string",
 85 | 																	description:
 86 | 																		"Modulus for RSA keys (base64url-encoded)",
 87 | 																	nullable: true,
 88 | 																},
 89 | 																e: {
 90 | 																	type: "string",
 91 | 																	description:
 92 | 																		"Exponent for RSA keys (base64url-encoded)",
 93 | 																	nullable: true,
 94 | 																},
 95 | 																crv: {
 96 | 																	type: "string",
 97 | 																	description:
 98 | 																		"Curve name for elliptic curve keys (e.g., 'Ed25519', 'P-256')",
 99 | 																	nullable: true,
100 | 																},
101 | 																x: {
102 | 																	type: "string",
103 | 																	description:
104 | 																		"X coordinate for elliptic curve keys (base64url-encoded)",
105 | 																	nullable: true,
106 | 																},
107 | 																y: {
108 | 																	type: "string",
109 | 																	description:
110 | 																		"Y coordinate for elliptic curve keys (base64url-encoded)",
111 | 																	nullable: true,
112 | 																},
113 | 															},
114 | 															required: ["kid", "kty", "alg"],
115 | 														},
116 | 													},
117 | 												},
118 | 												required: ["keys"],
119 | 											},
120 | 										},
121 | 									},
122 | 								},
123 | 							},
124 | 						},
125 | 					},
126 | 				},
127 | 				async (ctx) => {
128 | 					// Disables endpoint if using remote url strategy
129 | 					if (options?.jwks?.remoteUrl) {
130 | 						throw new APIError("NOT_FOUND");
131 | 					}
132 | 
133 | 					const adapter = getJwksAdapter(ctx.context.adapter);
134 | 
135 | 					const keySets = await adapter.getAllKeys();
136 | 
137 | 					if (keySets.length === 0) {
138 | 						const key = await createJwk(ctx, options);
139 | 						keySets.push(key);
140 | 					}
141 | 
142 | 					const keyPairConfig = options?.jwks?.keyPairConfig;
143 | 					const defaultCrv = keyPairConfig
144 | 						? "crv" in keyPairConfig
145 | 							? (keyPairConfig as { crv: string }).crv
146 | 							: undefined
147 | 						: undefined;
148 | 					return ctx.json({
149 | 						keys: keySets.map((keySet) => {
150 | 							return {
151 | 								alg: keySet.alg ?? options?.jwks?.keyPairConfig?.alg ?? "EdDSA",
152 | 								crv: keySet.crv ?? defaultCrv,
153 | 								...JSON.parse(keySet.publicKey),
154 | 								kid: keySet.id,
155 | 							};
156 | 						}),
157 | 					} satisfies JSONWebKeySet as JSONWebKeySet);
158 | 				},
159 | 			),
160 | 
161 | 			getToken: createAuthEndpoint(
162 | 				"/token",
163 | 				{
164 | 					method: "GET",
165 | 					requireHeaders: true,
166 | 					use: [sessionMiddleware],
167 | 					metadata: {
168 | 						openapi: {
169 | 							description: "Get a JWT token",
170 | 							responses: {
171 | 								200: {
172 | 									description: "Success",
173 | 									content: {
174 | 										"application/json": {
175 | 											schema: {
176 | 												type: "object",
177 | 												properties: {
178 | 													token: {
179 | 														type: "string",
180 | 													},
181 | 												},
182 | 											},
183 | 										},
184 | 									},
185 | 								},
186 | 							},
187 | 						},
188 | 					},
189 | 				},
190 | 				async (ctx) => {
191 | 					const jwt = await getJwtToken(ctx, options);
192 | 					return ctx.json({
193 | 						token: jwt,
194 | 					});
195 | 				},
196 | 			),
197 | 			signJWT: createAuthEndpoint(
198 | 				"/sign-jwt",
199 | 				{
200 | 					method: "POST",
201 | 					metadata: {
202 | 						SERVER_ONLY: true,
203 | 						$Infer: {
204 | 							body: {} as {
205 | 								payload: JWTPayload;
206 | 								overrideOptions?: JwtOptions;
207 | 							},
208 | 						},
209 | 					},
210 | 					body: z.object({
211 | 						payload: z.record(z.string(), z.any()),
212 | 						overrideOptions: z.record(z.string(), z.any()).optional(),
213 | 					}),
214 | 				},
215 | 				async (c) => {
216 | 					const jwt = await signJWT(c, {
217 | 						options: {
218 | 							...options,
219 | 							...c.body.overrideOptions,
220 | 						},
221 | 						payload: c.body.payload,
222 | 					});
223 | 					return c.json({ token: jwt });
224 | 				},
225 | 			),
226 | 		},
227 | 		hooks: {
228 | 			after: [
229 | 				{
230 | 					matcher(context) {
231 | 						return context.path === "/get-session";
232 | 					},
233 | 					handler: createAuthMiddleware(async (ctx) => {
234 | 						if (options?.disableSettingJwtHeader) {
235 | 							return;
236 | 						}
237 | 
238 | 						const session = ctx.context.session || ctx.context.newSession;
239 | 						if (session && session.session) {
240 | 							const jwt = await getJwtToken(ctx, options);
241 | 							const exposedHeaders =
242 | 								ctx.context.responseHeaders?.get(
243 | 									"access-control-expose-headers",
244 | 								) || "";
245 | 							const headersSet = new Set(
246 | 								exposedHeaders
247 | 									.split(",")
248 | 									.map((header) => header.trim())
249 | 									.filter(Boolean),
250 | 							);
251 | 							headersSet.add("set-auth-jwt");
252 | 							ctx.setHeader("set-auth-jwt", jwt);
253 | 							ctx.setHeader(
254 | 								"Access-Control-Expose-Headers",
255 | 								Array.from(headersSet).join(", "),
256 | 							);
257 | 						}
258 | 					}),
259 | 				},
260 | 			],
261 | 		},
262 | 		schema: mergeSchema(schema, options?.schema),
263 | 	} satisfies BetterAuthPlugin;
264 | };
265 | 
266 | export { getJwtToken };
267 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/plugins/mcp.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: MCP
  3 | description: MCP provider plugin for Better Auth
  4 | ---
  5 | 
  6 | `OAuth` `MCP`
  7 | 
  8 | The **MCP** plugin lets your app act as an OAuth provider for MCP clients. It handles authentication and makes it easy to issue and manage access tokens for MCP applications.
  9 | 
 10 | ## Installation
 11 | 
 12 | <Steps>
 13 |     <Step>
 14 |         ### Add the Plugin
 15 | 
 16 |         Add the MCP plugin to your auth configuration and specify the login page path.
 17 | 
 18 |         ```ts title="auth.ts"
 19 |         import { betterAuth } from "better-auth";
 20 |         import { mcp } from "better-auth/plugins";
 21 | 
 22 |         export const auth = betterAuth({
 23 |             plugins: [
 24 |                 mcp({
 25 |                     loginPage: "/sign-in" // path to your login page
 26 |                 })
 27 |             ]
 28 |         });
 29 |         ```
 30 |         <Callout>
 31 |             This doesn't have a client plugin, so you don't need to make any changes to your authClient.
 32 |         </Callout>
 33 |     </Step>
 34 | 
 35 |     <Step>
 36 |         ### Generate Schema
 37 | 
 38 |         Run the migration or generate the schema to add the necessary fields and tables to the database.
 39 | 
 40 |         <Tabs items={["migrate", "generate"]}>
 41 |             <Tab value="migrate">
 42 |             ```bash
 43 |             npx @better-auth/cli migrate
 44 |             ```
 45 |             </Tab>
 46 |             <Tab value="generate">
 47 |             ```bash
 48 |             npx @better-auth/cli generate
 49 |             ```
 50 |             </Tab>
 51 |         </Tabs>
 52 |         The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#schema) section for details.
 53 |     </Step>
 54 | </Steps>
 55 | 
 56 | ## Usage
 57 | 
 58 | ### OAuth Discovery Metadata
 59 | 
 60 | Better Auth already handles the `/api/auth/.well-known/oauth-authorization-server` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-authorization-server` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:
 61 | 
 62 | ```ts title=".well-known/oauth-authorization-server/route.ts"
 63 | import { oAuthDiscoveryMetadata } from "better-auth/plugins";
 64 | import { auth } from "../../../lib/auth";
 65 | 
 66 | export const GET = oAuthDiscoveryMetadata(auth);
 67 | ```
 68 | 
 69 | ### OAuth Protected Resource Metadata
 70 | 
 71 | Better Auth already handles the `/api/auth/.well-known/oauth-protected-resource` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-protected-resource` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:
 72 | 
 73 | ```ts title="/.well-known/oauth-protected-resource/route.ts"
 74 | import { oAuthProtectedResourceMetadata } from "better-auth/plugins";
 75 | import { auth } from "@/lib/auth";
 76 | 
 77 | export const GET = oAuthProtectedResourceMetadata(auth);
 78 | ```
 79 | 
 80 | ### MCP Session Handling
 81 | 
 82 | You can use the helper function `withMcpAuth` to get the session and handle unauthenticated calls automatically.
 83 | 
 84 | 
 85 | ```ts title="api/[transport]/route.ts"
 86 | import { auth } from "@/lib/auth";
 87 | import { createMcpHandler } from "@vercel/mcp-adapter";
 88 | import { withMcpAuth } from "better-auth/plugins";
 89 | import { z } from "zod";
 90 | 
 91 | const handler = withMcpAuth(auth, (req, session) => {
 92 |     // session contains the access token record with scopes and user ID
 93 |     return createMcpHandler(
 94 |         (server) => {
 95 |             server.tool(
 96 |                 "echo",
 97 |                 "Echo a message",
 98 |                 { message: z.string() },
 99 |                 async ({ message }) => {
100 |                     return {
101 |                         content: [{ type: "text", text: `Tool echo: ${message}` }],
102 |                     };
103 |                 },
104 |             );
105 |         },
106 |         {
107 |             capabilities: {
108 |                 tools: {
109 |                     echo: {
110 |                         description: "Echo a message",
111 |                     },
112 |                 },
113 |             },
114 |         },
115 |         {
116 |             redisUrl: process.env.REDIS_URL,
117 |             basePath: "/api",
118 |             verboseLogs: true,
119 |             maxDuration: 60,
120 |         },
121 |     )(req);
122 | });
123 | 
124 | export { handler as GET, handler as POST, handler as DELETE };
125 | ```
126 | 
127 | You can also use `auth.api.getMcpSession` to get the session using the access token sent from the MCP client:
128 | 
129 | ```ts title="api/[transport]/route.ts"
130 | import { auth } from "@/lib/auth";
131 | import { createMcpHandler } from "@vercel/mcp-adapter";
132 | import { z } from "zod";
133 | 
134 | const handler = async (req: Request) => {
135 |      // session contains the access token record with scopes and user ID
136 |     const session = await auth.api.getMcpSession({
137 |         headers: req.headers
138 |     })
139 |     if(!session){
140 |         //this is important and you must return 401
141 |         return new Response(null, {
142 |             status: 401
143 |         })
144 |     }
145 |     return createMcpHandler(
146 |         (server) => {
147 |             server.tool(
148 |                 "echo",
149 |                 "Echo a message",
150 |                 { message: z.string() },
151 |                 async ({ message }) => {
152 |                     return {
153 |                         content: [{ type: "text", text: `Tool echo: ${message}` }],
154 |                     };
155 |                 },
156 |             );
157 |         },
158 |         {
159 |             capabilities: {
160 |                 tools: {
161 |                     echo: {
162 |                         description: "Echo a message",
163 |                     },
164 |                 },
165 |             },
166 |         },
167 |         {
168 |             redisUrl: process.env.REDIS_URL,
169 |             basePath: "/api",
170 |             verboseLogs: true,
171 |             maxDuration: 60,
172 |         },
173 |     )(req);
174 | }
175 | 
176 | export { handler as GET, handler as POST, handler as DELETE };
177 | ```
178 | 
179 | ## Configuration
180 | 
181 | The MCP plugin accepts the following configuration options:
182 | 
183 | <TypeTable
184 |   type={{
185 |     loginPage: {
186 |         description: "Path to the login page where users will be redirected for authentication",
187 |         type: "string",
188 |         required: true
189 |     },
190 |     resource: {
191 |         description: "The resource that should be returned by the protected resource metadata endpoint",
192 |         type: "string",
193 |         required: false
194 |     },
195 |     oidcConfig: {
196 |         description: "Optional OIDC configuration options",
197 |         type: "object",
198 |         required: false
199 |     }
200 |   }}
201 | />
202 | 
203 | ### OIDC Configuration
204 | 
205 | The plugin supports additional OIDC configuration options through the `oidcConfig` parameter:
206 | 
207 | <TypeTable
208 |   type={{
209 |     codeExpiresIn: {
210 |         description: "Expiration time for authorization codes in seconds",
211 |         type: "number",
212 |         default: 600
213 |     },
214 |     accessTokenExpiresIn: {
215 |         description: "Expiration time for access tokens in seconds",
216 |         type: "number",
217 |         default: 3600
218 |     },
219 |     refreshTokenExpiresIn: {
220 |         description: "Expiration time for refresh tokens in seconds",
221 |         type: "number",
222 |         default: 604800
223 |     },
224 |     defaultScope: {
225 |         description: "Default scope for OAuth requests",
226 |         type: "string",
227 |         default: "openid"
228 |     },
229 |     scopes: {
230 |         description: "Additional scopes to support",
231 |         type: "string[]",
232 |         default: '["openid", "profile", "email", "offline_access"]'
233 |     }
234 |   }}
235 | />
236 | 
237 | ## Schema
238 | 
239 | The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#schema) section for details.
240 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/components/ui/menubar.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import * as React from "react";
  4 | import {
  5 | 	CheckIcon,
  6 | 	ChevronRightIcon,
  7 | 	DotFilledIcon,
  8 | } from "@radix-ui/react-icons";
  9 | import * as MenubarPrimitive from "@radix-ui/react-menubar";
 10 | 
 11 | import { cn } from "@/lib/utils";
 12 | 
 13 | const MenubarMenu = MenubarPrimitive.Menu;
 14 | 
 15 | const MenubarGroup = MenubarPrimitive.Group;
 16 | 
 17 | const MenubarPortal = MenubarPrimitive.Portal;
 18 | 
 19 | const MenubarSub = MenubarPrimitive.Sub;
 20 | 
 21 | const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
 22 | 
 23 | const Menubar = ({
 24 | 	ref,
 25 | 	className,
 26 | 	...props
 27 | }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> & {
 28 | 	ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Root>>;
 29 | }) => (
 30 | 	<MenubarPrimitive.Root
 31 | 		ref={ref}
 32 | 		className={cn(
 33 | 			"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
 34 | 			className,
 35 | 		)}
 36 | 		{...props}
 37 | 	/>
 38 | );
 39 | Menubar.displayName = MenubarPrimitive.Root.displayName;
 40 | 
 41 | const MenubarTrigger = ({
 42 | 	ref,
 43 | 	className,
 44 | 	...props
 45 | }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> & {
 46 | 	ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Trigger>>;
 47 | }) => (
 48 | 	<MenubarPrimitive.Trigger
 49 | 		ref={ref}
 50 | 		className={cn(
 51 | 			"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
 52 | 			className,
 53 | 		)}
 54 | 		{...props}
 55 | 	/>
 56 | );
 57 | MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
 58 | 
 59 | const MenubarSubTrigger = ({ ref, className, inset, children, ...props }) => (
 60 | 	<MenubarPrimitive.SubTrigger
 61 | 		ref={ref}
 62 | 		className={cn(
 63 | 			"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",
 64 | 			inset && "pl-8",
 65 | 			className,
 66 | 		)}
 67 | 		{...props}
 68 | 	>
 69 | 		{children}
 70 | 		<ChevronRightIcon className="ml-auto h-4 w-4" />
 71 | 	</MenubarPrimitive.SubTrigger>
 72 | );
 73 | MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
 74 | 
 75 | const MenubarSubContent = ({
 76 | 	ref,
 77 | 	className,
 78 | 	...props
 79 | }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> & {
 80 | 	ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.SubContent>>;
 81 | }) => (
 82 | 	<MenubarPrimitive.SubContent
 83 | 		ref={ref}
 84 | 		className={cn(
 85 | 			"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",
 86 | 			className,
 87 | 		)}
 88 | 		{...props}
 89 | 	/>
 90 | );
 91 | MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
 92 | 
 93 | const MenubarContent = ({
 94 | 	ref,
 95 | 	className,
 96 | 	align = "start",
 97 | 	alignOffset = -4,
 98 | 	sideOffset = 8,
 99 | 	...props
100 | }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> & {
101 | 	ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Content>>;
102 | }) => (
103 | 	<MenubarPrimitive.Portal>
104 | 		<MenubarPrimitive.Content
105 | 			ref={ref}
106 | 			align={align}
107 | 			alignOffset={alignOffset}
108 | 			sideOffset={sideOffset}
109 | 			className={cn(
110 | 				"z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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",
111 | 				className,
112 | 			)}
113 | 			{...props}
114 | 		/>
115 | 	</MenubarPrimitive.Portal>
116 | );
117 | MenubarContent.displayName = MenubarPrimitive.Content.displayName;
118 | 
119 | const MenubarItem = ({ ref, className, inset, ...props }) => (
120 | 	<MenubarPrimitive.Item
121 | 		ref={ref}
122 | 		className={cn(
123 | 			"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",
124 | 			inset && "pl-8",
125 | 			className,
126 | 		)}
127 | 		{...props}
128 | 	/>
129 | );
130 | MenubarItem.displayName = MenubarPrimitive.Item.displayName;
131 | 
132 | const MenubarCheckboxItem = ({
133 | 	ref,
134 | 	className,
135 | 	children,
136 | 	checked,
137 | 	...props
138 | }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> & {
139 | 	ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.CheckboxItem>>;
140 | }) => (
141 | 	<MenubarPrimitive.CheckboxItem
142 | 		ref={ref}
143 | 		className={cn(
144 | 			"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",
145 | 			className,
146 | 		)}
147 | 		checked={checked}
148 | 		{...props}
149 | 	>
150 | 		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
151 | 			<MenubarPrimitive.ItemIndicator>
152 | 				<CheckIcon className="h-4 w-4" />
153 | 			</MenubarPrimitive.ItemIndicator>
154 | 		</span>
155 | 		{children}
156 | 	</MenubarPrimitive.CheckboxItem>
157 | );
158 | MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
159 | 
160 | const MenubarRadioItem = ({
161 | 	ref,
162 | 	className,
163 | 	children,
164 | 	...props
165 | }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> & {
166 | 	ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.RadioItem>>;
167 | }) => (
168 | 	<MenubarPrimitive.RadioItem
169 | 		ref={ref}
170 | 		className={cn(
171 | 			"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",
172 | 			className,
173 | 		)}
174 | 		{...props}
175 | 	>
176 | 		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
177 | 			<MenubarPrimitive.ItemIndicator>
178 | 				<DotFilledIcon className="h-4 w-4 fill-current" />
179 | 			</MenubarPrimitive.ItemIndicator>
180 | 		</span>
181 | 		{children}
182 | 	</MenubarPrimitive.RadioItem>
183 | );
184 | MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
185 | 
186 | const MenubarLabel = ({ ref, className, inset, ...props }) => (
187 | 	<MenubarPrimitive.Label
188 | 		ref={ref}
189 | 		className={cn(
190 | 			"px-2 py-1.5 text-sm font-semibold",
191 | 			inset && "pl-8",
192 | 			className,
193 | 		)}
194 | 		{...props}
195 | 	/>
196 | );
197 | MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
198 | 
199 | const MenubarSeparator = ({
200 | 	ref,
201 | 	className,
202 | 	...props
203 | }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> & {
204 | 	ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Separator>>;
205 | }) => (
206 | 	<MenubarPrimitive.Separator
207 | 		ref={ref}
208 | 		className={cn("-mx-1 my-1 h-px bg-muted", className)}
209 | 		{...props}
210 | 	/>
211 | );
212 | MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
213 | 
214 | const MenubarShortcut = ({
215 | 	className,
216 | 	...props
217 | }: React.HTMLAttributes<HTMLSpanElement>) => {
218 | 	return (
219 | 		<span
220 | 			className={cn(
221 | 				"ml-auto text-xs tracking-widest text-muted-foreground",
222 | 				className,
223 | 			)}
224 | 			{...props}
225 | 		/>
226 | 	);
227 | };
228 | MenubarShortcut.displayname = "MenubarShortcut";
229 | 
230 | export {
231 | 	Menubar,
232 | 	MenubarMenu,
233 | 	MenubarTrigger,
234 | 	MenubarContent,
235 | 	MenubarItem,
236 | 	MenubarSeparator,
237 | 	MenubarLabel,
238 | 	MenubarCheckboxItem,
239 | 	MenubarRadioGroup,
240 | 	MenubarRadioItem,
241 | 	MenubarPortal,
242 | 	MenubarSubContent,
243 | 	MenubarSubTrigger,
244 | 	MenubarGroup,
245 | 	MenubarSub,
246 | 	MenubarShortcut,
247 | };
248 | 
```

--------------------------------------------------------------------------------
/docs/app/docs/lib/get-llm-text.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { remark } from "remark";
  2 | import remarkGfm from "remark-gfm";
  3 | import { fileGenerator, remarkDocGen } from "fumadocs-docgen";
  4 | import { remarkNpm } from "fumadocs-core/mdx-plugins";
  5 | import remarkStringify from "remark-stringify";
  6 | import remarkMdx from "remark-mdx";
  7 | import { remarkAutoTypeTable } from "fumadocs-typescript";
  8 | import { remarkInclude } from "fumadocs-mdx/config";
  9 | import { readFile } from "fs/promises";
 10 | 
 11 | function extractAPIMethods(rawContent: string): string {
 12 | 	const apiMethodRegex = /<APIMethod\s+([^>]+)>([\s\S]*?)<\/APIMethod>/g;
 13 | 
 14 | 	return rawContent.replace(apiMethodRegex, (match, attributes, content) => {
 15 | 		// Parse attributes by matching
 16 | 		const pathMatch = attributes.match(/path="([^"]+)"/);
 17 | 		const methodMatch = attributes.match(/method="([^"]+)"/);
 18 | 		const requireSessionMatch = attributes.match(/requireSession/);
 19 | 		const isServerOnlyMatch = attributes.match(/isServerOnly/);
 20 | 		const isClientOnlyMatch = attributes.match(/isClientOnly/);
 21 | 		const noResultMatch = attributes.match(/noResult/);
 22 | 		const resultVariableMatch = attributes.match(/resultVariable="([^"]+)"/);
 23 | 		const forceAsBodyMatch = attributes.match(/forceAsBody/);
 24 | 		const forceAsQueryMatch = attributes.match(/forceAsQuery/);
 25 | 
 26 | 		const path = pathMatch ? pathMatch[1] : "";
 27 | 		const method = methodMatch ? methodMatch[1] : "GET";
 28 | 		const requireSession = !!requireSessionMatch;
 29 | 		const isServerOnly = !!isServerOnlyMatch;
 30 | 		const isClientOnly = !!isClientOnlyMatch;
 31 | 		const noResult = !!noResultMatch;
 32 | 		const resultVariable = resultVariableMatch
 33 | 			? resultVariableMatch[1]
 34 | 			: "data";
 35 | 		const forceAsBody = !!forceAsBodyMatch;
 36 | 		const forceAsQuery = !!forceAsQueryMatch;
 37 | 
 38 | 		const typeMatch = content.match(/type\s+(\w+)\s*=\s*\{([\s\S]*?)\}/);
 39 | 		if (!typeMatch) {
 40 | 			return match; // Return original if no type found
 41 | 		}
 42 | 
 43 | 		const functionName = typeMatch[1];
 44 | 		const typeBody = typeMatch[2];
 45 | 
 46 | 		const properties = parseTypeBody(typeBody);
 47 | 
 48 | 		const clientCode = generateClientCode(functionName, properties, path);
 49 | 		const serverCode = generateServerCode(
 50 | 			functionName,
 51 | 			properties,
 52 | 			method,
 53 | 			requireSession,
 54 | 			forceAsBody,
 55 | 			forceAsQuery,
 56 | 			noResult,
 57 | 			resultVariable,
 58 | 		);
 59 | 
 60 | 		return `
 61 | ### Client Side
 62 | 
 63 | \`\`\`ts
 64 | ${clientCode}
 65 | \`\`\`
 66 | 
 67 | ### Server Side
 68 | 
 69 | \`\`\`ts
 70 | ${serverCode}
 71 | \`\`\`
 72 | 
 73 | ### Type Definition
 74 | 
 75 | \`\`\`ts
 76 | type ${functionName} = {${typeBody}
 77 | }
 78 | \`\`\`
 79 | `;
 80 | 	});
 81 | }
 82 | 
 83 | function parseTypeBody(typeBody: string) {
 84 | 	const properties: Array<{
 85 | 		name: string;
 86 | 		type: string;
 87 | 		required: boolean;
 88 | 		description: string;
 89 | 		exampleValue: string;
 90 | 		isServerOnly: boolean;
 91 | 		isClientOnly: boolean;
 92 | 	}> = [];
 93 | 
 94 | 	const lines = typeBody.split("\n");
 95 | 
 96 | 	for (const line of lines) {
 97 | 		const trimmed = line.trim();
 98 | 
 99 | 		if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*"))
100 | 			continue;
101 | 		const propMatch = trimmed.match(
102 | 			/^(\w+)(\?)?:\s*(.+?)(\s*=\s*["']([^"']+)["'])?(\s*\/\/\s*(.+))?$/,
103 | 		);
104 | 		if (propMatch) {
105 | 			const [, name, optional, type, , exampleValue, , description] = propMatch;
106 | 
107 | 			let cleanType = type.trim();
108 | 			let cleanExampleValue = exampleValue || "";
109 | 
110 | 			cleanType = cleanType.replace(/,$/, "");
111 | 
112 | 			properties.push({
113 | 				name,
114 | 				type: cleanType,
115 | 				required: !optional,
116 | 				description: description || "",
117 | 				exampleValue: cleanExampleValue,
118 | 				isServerOnly: false,
119 | 				isClientOnly: false,
120 | 			});
121 | 		}
122 | 	}
123 | 
124 | 	return properties;
125 | }
126 | 
127 | // Generate client code example
128 | function generateClientCode(
129 | 	functionName: string,
130 | 	properties: any[],
131 | 	path: string,
132 | ) {
133 | 	if (!functionName || !path) {
134 | 		return "// Unable to generate client code - missing function name or path";
135 | 	}
136 | 
137 | 	const clientMethodPath = pathToDotNotation(path);
138 | 	const body = createClientBody(properties);
139 | 
140 | 	return `const { data, error } = await authClient.${clientMethodPath}(${body});`;
141 | }
142 | 
143 | // Generate server code example
144 | function generateServerCode(
145 | 	functionName: string,
146 | 	properties: any[],
147 | 	method: string,
148 | 	requireSession: boolean,
149 | 	forceAsBody: boolean,
150 | 	forceAsQuery: boolean,
151 | 	noResult: boolean,
152 | 	resultVariable: string,
153 | ) {
154 | 	if (!functionName) {
155 | 		return "// Unable to generate server code - missing function name";
156 | 	}
157 | 
158 | 	const body = createServerBody(
159 | 		properties,
160 | 		method,
161 | 		requireSession,
162 | 		forceAsBody,
163 | 		forceAsQuery,
164 | 	);
165 | 
166 | 	return `${noResult ? "" : `const ${resultVariable} = `}await auth.api.${functionName}(${body});`;
167 | }
168 | 
169 | function pathToDotNotation(input: string): string {
170 | 	return input
171 | 		.split("/")
172 | 		.filter(Boolean)
173 | 		.map((segment) =>
174 | 			segment
175 | 				.split("-")
176 | 				.map((word, i) =>
177 | 					i === 0
178 | 						? word.toLowerCase()
179 | 						: word.charAt(0).toUpperCase() + word.slice(1),
180 | 				)
181 | 				.join(""),
182 | 		)
183 | 		.join(".");
184 | }
185 | 
186 | // Helper function to create client body (simplified version)
187 | function createClientBody(props: any[]) {
188 | 	if (props.length === 0) return "{}";
189 | 
190 | 	let body = "{\n";
191 | 
192 | 	for (const prop of props) {
193 | 		if (prop.isServerOnly) continue;
194 | 
195 | 		let comment = "";
196 | 		if (!prop.required || prop.description) {
197 | 			const comments = [];
198 | 			if (!prop.required) comments.push("required");
199 | 			if (prop.description) comments.push(prop.description);
200 | 			comment = ` // ${comments.join(", ")}`;
201 | 		}
202 | 
203 | 		body += `    ${prop.name}${prop.exampleValue ? `: ${prop.exampleValue}` : ""}${prop.type === "Object" ? ": {}" : ""},${comment}\n`;
204 | 	}
205 | 
206 | 	body += "}";
207 | 	return body;
208 | }
209 | 
210 | function createServerBody(
211 | 	props: any[],
212 | 	method: string,
213 | 	requireSession: boolean,
214 | 	forceAsBody: boolean,
215 | 	forceAsQuery: boolean,
216 | ) {
217 | 	const relevantProps = props.filter((x) => !x.isClientOnly);
218 | 
219 | 	if (relevantProps.length === 0 && !requireSession) {
220 | 		return "{}";
221 | 	}
222 | 
223 | 	let serverBody = "{\n";
224 | 
225 | 	if (relevantProps.length > 0) {
226 | 		const bodyKey =
227 | 			(method === "POST" || forceAsBody) && !forceAsQuery ? "body" : "query";
228 | 		serverBody += `    ${bodyKey}: {\n`;
229 | 
230 | 		for (const prop of relevantProps) {
231 | 			let comment = "";
232 | 			if (!prop.required || prop.description) {
233 | 				const comments = [];
234 | 				if (!prop.required) comments.push("required");
235 | 				if (prop.description) comments.push(prop.description);
236 | 				comment = ` // ${comments.join(", ")}`;
237 | 			}
238 | 
239 | 			serverBody += `        ${prop.name}${prop.exampleValue ? `: ${prop.exampleValue}` : ""}${prop.type === "Object" ? ": {}" : ""},${comment}\n`;
240 | 		}
241 | 
242 | 		serverBody += "    }";
243 | 	}
244 | 
245 | 	if (requireSession) {
246 | 		if (relevantProps.length > 0) serverBody += ",";
247 | 		serverBody +=
248 | 			"\n    // This endpoint requires session cookies.\n    headers: await headers()";
249 | 	}
250 | 
251 | 	serverBody += "\n}";
252 | 	return serverBody;
253 | }
254 | 
255 | const processor = remark()
256 | 	.use(remarkMdx)
257 | 	.use(remarkInclude)
258 | 	.use(remarkGfm)
259 | 	.use(remarkAutoTypeTable)
260 | 	.use(remarkDocGen, { generators: [fileGenerator()] })
261 | 	.use(remarkNpm)
262 | 	.use(remarkStringify);
263 | 
264 | export async function getLLMText(docPage: any) {
265 | 	const category = [docPage.slugs[0]];
266 | 
267 | 	// Read the raw file content
268 | 	const rawContent = await readFile(docPage.data._file.absolutePath, "utf-8");
269 | 
270 | 	// Extract APIMethod components & other nested wrapper before processing
271 | 	const processedContent = extractAPIMethods(rawContent);
272 | 
273 | 	const processed = await processor.process({
274 | 		path: docPage.data._file.absolutePath,
275 | 		value: processedContent,
276 | 	});
277 | 
278 | 	return `# ${category}: ${docPage.data.title}
279 | URL: ${docPage.url}
280 | Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/${
281 | 		docPage.file.path
282 | 	}
283 | 
284 | ${docPage.data.description}
285 |         
286 | ${processed.toString()}
287 | `;
288 | }
289 | 
```
Page 22/69FirstPrevNextLast