#
tokens: 48939/50000 14/1099 files (page 19/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 19 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-decelration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   └── index.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
│   │   ├── tsconfig.test.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── api
│   │   │   │   └── index.ts
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── endpoint-context.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── transaction.ts
│   │   │   ├── db
│   │   │   │   ├── adapter
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── plugin.ts
│   │   │   │   ├── schema
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── user.ts
│   │   │   │   │   └── verification.ts
│   │   │   │   └── type.ts
│   │   │   ├── env
│   │   │   │   ├── color-depth.ts
│   │   │   │   ├── env-impl.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.test.ts
│   │   │   │   └── logger.ts
│   │   │   ├── error
│   │   │   │   ├── codes.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/docs/content/docs/integrations/lynx.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Lynx Integration
  3 | description: Integrate Better Auth with Lynx cross-platform framework.
  4 | ---
  5 | 
  6 | This integration guide is for using Better Auth with [Lynx](https://lynxjs.org), a cross-platform rendering framework that enables developers to build applications for Android, iOS, and Web platforms with native rendering performance.
  7 | 
  8 | Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
  9 | 
 10 | ## Installation
 11 | 
 12 | Install Better Auth and the Lynx React dependency:
 13 | 
 14 | ```package-install
 15 | better-auth @lynx-js/react
 16 | ```
 17 | 
 18 | ## Create Client Instance
 19 | 
 20 | Import `createAuthClient` from `better-auth/lynx` to create your client instance:
 21 | 
 22 | ```ts title="lib/auth-client.ts"
 23 | import { createAuthClient } from "better-auth/lynx"
 24 | 
 25 | export const authClient = createAuthClient({
 26 |     baseURL: "http://localhost:3000" // The base URL of your auth server
 27 | })
 28 | ```
 29 | 
 30 | ## Usage
 31 | 
 32 | The Lynx client provides the same API as other Better Auth clients, with optimized integration for Lynx's reactive system.
 33 | 
 34 | ### Authentication Methods
 35 | 
 36 | ```ts
 37 | import { authClient } from "./lib/auth-client"
 38 | 
 39 | // Sign in with email and password
 40 | await authClient.signIn.email({
 41 |     email: "[email protected]",
 42 |     password: "password1234"
 43 | })
 44 | 
 45 | // Sign up
 46 | await authClient.signUp.email({
 47 |     email: "[email protected]", 
 48 |     password: "password1234",
 49 |     name: "John Doe"
 50 | })
 51 | 
 52 | // Sign out
 53 | await authClient.signOut()
 54 | ```
 55 | 
 56 | ### Hooks
 57 | 
 58 | The Lynx client includes reactive hooks that integrate seamlessly with Lynx's component system:
 59 | 
 60 | #### useSession
 61 | 
 62 | ```tsx title="components/user.tsx"
 63 | import { authClient } from "../lib/auth-client"
 64 | 
 65 | export function User() {
 66 |     const {
 67 |         data: session,
 68 |         isPending, // loading state
 69 |         error // error object 
 70 |     } = authClient.useSession()
 71 | 
 72 |     if (isPending) return <div>Loading...</div>
 73 |     if (error) return <div>Error: {error.message}</div>
 74 | 
 75 |     return (
 76 |         <div>
 77 |             {session ? (
 78 |                 <div>
 79 |                     <p>Welcome, {session.user.name}!</p>
 80 |                     <button onClick={() => authClient.signOut()}>
 81 |                         Sign Out
 82 |                     </button>
 83 |                 </div>
 84 |             ) : (
 85 |                 <button onClick={() => authClient.signIn.social({
 86 |                     provider: 'github'
 87 |                 })}>
 88 |                     Sign In with GitHub
 89 |                 </button>
 90 |             )}
 91 |         </div>
 92 |     )
 93 | }
 94 | ```
 95 | 
 96 | ### Store Integration
 97 | 
 98 | The Lynx client uses [nanostores](https://github.com/nanostores/nanostores) for state management and provides a `useStore` hook for accessing reactive state:
 99 | 
100 | ```tsx title="components/session-info.tsx"
101 | import { useStore } from "better-auth/lynx"
102 | import { authClient } from "../lib/auth-client"
103 | 
104 | export function SessionInfo() {
105 |     // Access the session store directly
106 |     const session = useStore(authClient.$store.session)
107 |     
108 |     return (
109 |         <div>
110 |             {session && (
111 |                 <pre>{JSON.stringify(session, null, 2)}</pre>
112 |             )}
113 |         </div>
114 |     )
115 | }
116 | ```
117 | 
118 | ### Advanced Store Usage
119 | 
120 | You can use the store with selective key watching for optimized re-renders:
121 | 
122 | ```tsx title="components/optimized-user.tsx"
123 | import { useStore } from "better-auth/lynx"
124 | import { authClient } from "../lib/auth-client"
125 | 
126 | export function OptimizedUser() {
127 |     // Only re-render when specific keys change
128 |     const session = useStore(authClient.$store.session, {
129 |         keys: ['user.name', 'user.email'] // Only watch these specific keys
130 |     })
131 |     
132 |     return (
133 |         <div>
134 |             {session?.user && (
135 |                 <div>
136 |                     <h2>{session.user.name}</h2>
137 |                     <p>{session.user.email}</p>
138 |                 </div>
139 |             )}
140 |         </div>
141 |     )
142 | }
143 | ```
144 | 
145 | ## Plugin Support
146 | 
147 | The Lynx client supports all Better Auth plugins:
148 | 
149 | ```ts title="lib/auth-client.ts"
150 | import { createAuthClient } from "better-auth/lynx"
151 | import { magicLinkClient } from "better-auth/client/plugins"
152 | 
153 | const authClient = createAuthClient({
154 |     plugins: [
155 |         magicLinkClient()
156 |     ]
157 | })
158 | 
159 | // Use plugin methods
160 | await authClient.signIn.magicLink({
161 |     email: "[email protected]"
162 | })
163 | ```
164 | 
165 | ## Error Handling
166 | 
167 | Error handling works the same as other Better Auth clients:
168 | 
169 | ```tsx title="components/login-form.tsx"
170 | import { authClient } from "../lib/auth-client"
171 | 
172 | export function LoginForm() {
173 |     const signIn = async (email: string, password: string) => {
174 |         const { data, error } = await authClient.signIn.email({
175 |             email,
176 |             password
177 |         })
178 |         
179 |         if (error) {
180 |             console.error('Login failed:', error.message)
181 |             return
182 |         }
183 |         
184 |         console.log('Login successful:', data)
185 |     }
186 |     
187 |     return (
188 |         <form onSubmit={(e) => {
189 |             e.preventDefault()
190 |             const formData = new FormData(e.target)
191 |             signIn(formData.get('email'), formData.get('password'))
192 |         }}>
193 |             <input name="email" type="email" placeholder="Email" />
194 |             <input name="password" type="password" placeholder="Password" />
195 |             <button type="submit">Sign In</button>
196 |         </form>
197 |     )
198 | }
199 | ```
200 | 
201 | ## Features
202 | 
203 | The Lynx client provides:
204 | 
205 | - **Cross-Platform Support**: Works across Android, iOS, and Web platforms
206 | - **Optimized Performance**: Built specifically for Lynx's reactive system
207 | - **Nanostores Integration**: Uses nanostores for efficient state management  
208 | - **Selective Re-rendering**: Watch specific store keys to minimize unnecessary updates
209 | - **Full API Compatibility**: All Better Auth methods and plugins work seamlessly
210 | - **TypeScript Support**: Full type safety with TypeScript inference
211 | 
212 | The Lynx integration maintains all the features and benefits of Better Auth while providing optimal performance and developer experience within Lynx's cross-platform ecosystem.
```

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

```typescript
  1 | import * as z from "zod";
  2 | import { APIError, sessionMiddleware } from "../../../api";
  3 | import { createAuthEndpoint } from "@better-auth/core/api";
  4 | import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
  5 | import type { apiKeySchema } from "../schema";
  6 | import type { ApiKey } from "../types";
  7 | import type { PredefinedApiKeyOptions } from ".";
  8 | import { safeJSONParse } from "../../../utils/json";
  9 | import type { AuthContext } from "@better-auth/core";
 10 | 
 11 | export function getApiKey({
 12 | 	opts,
 13 | 	schema,
 14 | 	deleteAllExpiredApiKeys,
 15 | }: {
 16 | 	opts: PredefinedApiKeyOptions;
 17 | 	schema: ReturnType<typeof apiKeySchema>;
 18 | 	deleteAllExpiredApiKeys(
 19 | 		ctx: AuthContext,
 20 | 		byPassLastCheckTime?: boolean,
 21 | 	): void;
 22 | }) {
 23 | 	return createAuthEndpoint(
 24 | 		"/api-key/get",
 25 | 		{
 26 | 			method: "GET",
 27 | 			query: z.object({
 28 | 				id: z.string().meta({
 29 | 					description: "The id of the Api Key",
 30 | 				}),
 31 | 			}),
 32 | 			use: [sessionMiddleware],
 33 | 			metadata: {
 34 | 				openapi: {
 35 | 					description: "Retrieve an existing API key by ID",
 36 | 					responses: {
 37 | 						"200": {
 38 | 							description: "API key retrieved successfully",
 39 | 							content: {
 40 | 								"application/json": {
 41 | 									schema: {
 42 | 										type: "object",
 43 | 										properties: {
 44 | 											id: {
 45 | 												type: "string",
 46 | 												description: "ID",
 47 | 											},
 48 | 											name: {
 49 | 												type: "string",
 50 | 												nullable: true,
 51 | 												description: "The name of the key",
 52 | 											},
 53 | 											start: {
 54 | 												type: "string",
 55 | 												nullable: true,
 56 | 												description:
 57 | 													"Shows the first few characters of the API key, including the prefix. This allows you to show those few characters in the UI to make it easier for users to identify the API key.",
 58 | 											},
 59 | 											prefix: {
 60 | 												type: "string",
 61 | 												nullable: true,
 62 | 												description:
 63 | 													"The API Key prefix. Stored as plain text.",
 64 | 											},
 65 | 											userId: {
 66 | 												type: "string",
 67 | 												description: "The owner of the user id",
 68 | 											},
 69 | 											refillInterval: {
 70 | 												type: "number",
 71 | 												nullable: true,
 72 | 												description:
 73 | 													"The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)",
 74 | 											},
 75 | 											refillAmount: {
 76 | 												type: "number",
 77 | 												nullable: true,
 78 | 												description: "The amount to refill",
 79 | 											},
 80 | 											lastRefillAt: {
 81 | 												type: "string",
 82 | 												format: "date-time",
 83 | 												nullable: true,
 84 | 												description: "The last refill date",
 85 | 											},
 86 | 											enabled: {
 87 | 												type: "boolean",
 88 | 												description: "Sets if key is enabled or disabled",
 89 | 												default: true,
 90 | 											},
 91 | 											rateLimitEnabled: {
 92 | 												type: "boolean",
 93 | 												description:
 94 | 													"Whether the key has rate limiting enabled",
 95 | 											},
 96 | 											rateLimitTimeWindow: {
 97 | 												type: "number",
 98 | 												nullable: true,
 99 | 												description: "The duration in milliseconds",
100 | 											},
101 | 											rateLimitMax: {
102 | 												type: "number",
103 | 												nullable: true,
104 | 												description:
105 | 													"Maximum amount of requests allowed within a window",
106 | 											},
107 | 											requestCount: {
108 | 												type: "number",
109 | 												description:
110 | 													"The number of requests made within the rate limit time window",
111 | 											},
112 | 											remaining: {
113 | 												type: "number",
114 | 												nullable: true,
115 | 												description:
116 | 													"Remaining requests (every time api key is used this should updated and should be updated on refill as well)",
117 | 											},
118 | 											lastRequest: {
119 | 												type: "string",
120 | 												format: "date-time",
121 | 												nullable: true,
122 | 												description: "When last request occurred",
123 | 											},
124 | 											expiresAt: {
125 | 												type: "string",
126 | 												format: "date-time",
127 | 												nullable: true,
128 | 												description: "Expiry date of a key",
129 | 											},
130 | 											createdAt: {
131 | 												type: "string",
132 | 												format: "date-time",
133 | 												description: "created at",
134 | 											},
135 | 											updatedAt: {
136 | 												type: "string",
137 | 												format: "date-time",
138 | 												description: "updated at",
139 | 											},
140 | 											metadata: {
141 | 												type: "object",
142 | 												nullable: true,
143 | 												additionalProperties: true,
144 | 												description: "Extra metadata about the apiKey",
145 | 											},
146 | 											permissions: {
147 | 												type: "string",
148 | 												nullable: true,
149 | 												description:
150 | 													"Permissions for the api key (stored as JSON string)",
151 | 											},
152 | 										},
153 | 										required: [
154 | 											"id",
155 | 											"userId",
156 | 											"enabled",
157 | 											"rateLimitEnabled",
158 | 											"requestCount",
159 | 											"createdAt",
160 | 											"updatedAt",
161 | 										],
162 | 									},
163 | 								},
164 | 							},
165 | 						},
166 | 					},
167 | 				},
168 | 			},
169 | 		},
170 | 		async (ctx) => {
171 | 			const { id } = ctx.query;
172 | 
173 | 			const session = ctx.context.session;
174 | 
175 | 			let apiKey = await ctx.context.adapter.findOne<ApiKey>({
176 | 				model: API_KEY_TABLE_NAME,
177 | 				where: [
178 | 					{
179 | 						field: "id",
180 | 						value: id,
181 | 					},
182 | 					{
183 | 						field: "userId",
184 | 						value: session.user.id,
185 | 					},
186 | 				],
187 | 			});
188 | 
189 | 			if (!apiKey) {
190 | 				throw new APIError("NOT_FOUND", {
191 | 					message: ERROR_CODES.KEY_NOT_FOUND,
192 | 				});
193 | 			}
194 | 
195 | 			deleteAllExpiredApiKeys(ctx.context);
196 | 
197 | 			// convert metadata string back to object
198 | 			apiKey.metadata = schema.apikey.fields.metadata.transform.output(
199 | 				apiKey.metadata as never as string,
200 | 			);
201 | 
202 | 			const { key, ...returningApiKey } = apiKey;
203 | 
204 | 			return ctx.json({
205 | 				...returningApiKey,
206 | 				permissions: returningApiKey.permissions
207 | 					? safeJSONParse<{
208 | 							[key: string]: string[];
209 | 						}>(returningApiKey.permissions)
210 | 					: null,
211 | 			});
212 | 		},
213 | 	);
214 | }
215 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/integrations/nitro.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Nitro Integration
  3 | description: Integrate Better Auth with Nitro.
  4 | ---
  5 | 
  6 | Better Auth can be integrated with your [Nitro Application](https://nitro.build/) (an open source framework to build web servers).
  7 | 
  8 | This guide aims to help you integrate Better Auth with your Nitro application in a few simple steps.
  9 | 
 10 | ## Create a new Nitro Application
 11 | 
 12 | Start by scaffolding a new Nitro application using the following command:
 13 | 
 14 | ```bash title="Terminal"
 15 | npx giget@latest nitro nitro-app --install
 16 | ```
 17 | 
 18 | This will create the `nitro-app` directory and install all the dependencies. You can now open the `nitro-app` directory in your code editor.
 19 | 
 20 | ### Prisma Adapter Setup
 21 | 
 22 | <Callout>
 23 |   This guide assumes that you have a basic understanding of Prisma. If you are new to Prisma, you can check out the [Prisma documentation](https://www.prisma.io/docs/getting-started).
 24 | 
 25 |   The `sqlite` database used in this guide will not work in a production environment. You should replace it with a production-ready database like `PostgreSQL`.
 26 | </Callout>
 27 | 
 28 | For this guide, we will be using the Prisma adapter. You can install prisma client by running the following command:
 29 | 
 30 | ```package-install
 31 | @prisma/client
 32 | ```
 33 | 
 34 | `prisma` can be installed as a dev dependency using the following command:
 35 | 
 36 | ```package-install
 37 | -D prisma
 38 | ```
 39 | 
 40 | Generate a `schema.prisma` file in the `prisma` directory by running the following command:
 41 | 
 42 | ```bash title="Terminal"
 43 | npx prisma init
 44 | ```
 45 | 
 46 | You can now replace the contents of the `schema.prisma` file with the following:
 47 | 
 48 | ```prisma title="prisma/schema.prisma"
 49 | generator client {
 50 |   provider = "prisma-client-js"
 51 | }
 52 | 
 53 | datasource db {
 54 |   provider = "sqlite"
 55 |   url      = env("DATABASE_URL")
 56 | }
 57 | 
 58 | // Will be deleted. Just need it to generate the prisma client
 59 | model Test {
 60 |   id   Int    @id @default(autoincrement())
 61 |   name String
 62 | }
 63 | ```
 64 | 
 65 | Ensure that you update the `DATABASE_URL` in your `.env` file to point to the location of your database.
 66 | 
 67 | ```txt title=".env"
 68 | DATABASE_URL="file:./dev.db"
 69 | ```
 70 | 
 71 | Run the following command to generate the Prisma client & sync the database:
 72 | 
 73 | ```bash title="Terminal"
 74 | npx prisma db push
 75 | ```
 76 | 
 77 | ### Install & Configure Better Auth
 78 | 
 79 | Follow steps 1 & 2 from the [installation guide](/docs/installation) to install Better Auth in your Nitro application & set up the environment variables.
 80 | 
 81 | Once that is done, create your Better Auth instance within the `server/utils/auth.ts` file.
 82 | 
 83 | ```ts title="server/utils/auth.ts"
 84 | import { betterAuth } from "better-auth";
 85 | import { prismaAdapter } from "better-auth/adapters/prisma";
 86 | import { PrismaClient } from "@prisma/client";
 87 | 
 88 | const prisma = new PrismaClient();
 89 | export const auth = betterAuth({
 90 |   database: prismaAdapter(prisma, { provider: "sqlite" }),
 91 |   emailAndPassword: { enabled: true },
 92 | });
 93 | ```
 94 | 
 95 | ### Update Prisma Schema
 96 | 
 97 | Use the Better Auth CLI to update your Prisma schema with the required models by running the following command:
 98 | 
 99 | ```bash title="Terminal"
100 | npx @better-auth/cli generate --config server/utils/auth.ts
101 | ```
102 | 
103 | <Callout>
104 |   The `--config` flag is used to specify the path to the file where you have created your Better Auth instance.
105 | </Callout>
106 | 
107 | Head over to the `prisma/schema.prisma` file & save the file to trigger the format on save.
108 | 
109 | After saving the file, you can run the `npx prisma db push` command to update the database schema.
110 | 
111 | ## Mount The Handler
112 | 
113 | You can now mount the Better Auth handler in your Nitro application. You can do this by adding the following code to your `server/routes/api/auth/[...all].ts` file:
114 | 
115 | ```ts title="server/routes/api/auth/[...all].ts"
116 | export default defineEventHandler((event) => {
117 |   return auth.handler(toWebRequest(event));
118 | });
119 | ```
120 | <Callout>
121 |   This is a [catch-all](https://nitro.build/guide/routing#catch-all-route) route that will handle all requests to `/api/auth/*`.
122 | </Callout>
123 | 
124 | ### CORS
125 | 
126 | You can configure CORS for your Nitro app by creating a plugin.
127 | 
128 | Start by installing the cors package:
129 | 
130 | ```package-install
131 | cors
132 | ```
133 | 
134 | You can now create a new file `server/plugins/cors.ts` and add the following code:
135 | 
136 | ```ts title="server/plugins/cors.ts"
137 | import cors from "cors";
138 | export default defineNitroPlugin((plugin) => {
139 |   plugin.h3App.use(
140 |     fromNodeMiddleware(
141 |       cors({
142 |         origin: "*",
143 |       }),
144 |     ),
145 |   );
146 | });
147 | ```
148 | <Callout>
149 |   This will enable CORS for all routes. You can customize the `origin` property to allow requests from specific domains. Ensure that the config is in sync with your frontend application.
150 | </Callout>
151 | 
152 | ### Auth Guard/Middleware
153 | 
154 | You can add an auth guard to your Nitro application to protect routes that require authentication. You can do this by creating a new file `server/utils/require-auth.ts` and adding the following code:
155 | 
156 | ```ts title="server/utils/require-auth.ts"
157 | import { EventHandler, H3Event } from "h3";
158 | import { fromNodeHeaders } from "better-auth/node";
159 | 
160 | /**
161 |  * Middleware used to require authentication for a route.
162 |  *
163 |  * Can be extended to check for specific roles or permissions.
164 |  */
165 | export const requireAuth: EventHandler = async (event: H3Event) => {
166 |   const headers = event.headers;
167 | 
168 |   const session = await auth.api.getSession({
169 |     headers: headers,
170 |   });
171 |   if (!session)
172 |     throw createError({
173 |       statusCode: 401,
174 |       statusMessage: "Unauthorized",
175 |     });
176 |   // You can save the session to the event context for later use
177 |   event.context.auth = session;
178 | };
179 | ```
180 | 
181 | You can now use this event handler/middleware in your routes to protect them:
182 | 
183 | ```ts title="server/routes/api/secret.get.ts"
184 | // Object syntax of the route handler
185 | export default defineEventHandler({
186 |   // The user has to be logged in to access this route
187 |   onRequest: [requireAuth],
188 |   handler: async (event) => {
189 |     setResponseStatus(event, 201, "Secret data");
190 |     return { message: "Secret data" };
191 |   },
192 | });
193 | ```
194 | 
195 | ### Example
196 | 
197 | You can find an example of a Nitro application integrated with Better Auth & Prisma [here](https://github.com/BayBreezy/nitrojs-better-auth-prisma).
198 | 
```

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

```typescript
  1 | import { betterFetch } from "@better-fetch/fetch";
  2 | import type { OAuthProvider, ProviderOptions } from "../oauth2";
  3 | import {
  4 | 	createAuthorizationURL,
  5 | 	refreshAccessToken,
  6 | 	validateAuthorizationCode,
  7 | } from "../oauth2";
  8 | 
  9 | export interface TwitterProfile {
 10 | 	data: {
 11 | 		/**
 12 | 		 * Unique identifier of this user. This is returned as a string in order to avoid complications with languages and tools
 13 | 		 * that cannot handle large integers.
 14 | 		 */
 15 | 		id: string;
 16 | 		/** The friendly name of this user, as shown on their profile. */
 17 | 		name: string;
 18 | 		/** The email address of this user. */
 19 | 		email?: string;
 20 | 		/** The Twitter handle (screen name) of this user. */
 21 | 		username: string;
 22 | 		/**
 23 | 		 * The location specified in the user's profile, if the user provided one.
 24 | 		 * As this is a freeform value, it may not indicate a valid location, but it may be fuzzily evaluated when performing searches with location queries.
 25 | 		 *
 26 | 		 * To return this field, add `user.fields=location` in the authorization request's query parameter.
 27 | 		 */
 28 | 		location?: string;
 29 | 		/**
 30 | 		 * This object and its children fields contain details about text that has a special meaning in the user's description.
 31 | 		 *
 32 | 		 *To return this field, add `user.fields=entities` in the authorization request's query parameter.
 33 | 		 */
 34 | 		entities?: {
 35 | 			/** Contains details about the user's profile website. */
 36 | 			url: {
 37 | 				/** Contains details about the user's profile website. */
 38 | 				urls: Array<{
 39 | 					/** The start position (zero-based) of the recognized user's profile website. All start indices are inclusive. */
 40 | 					start: number;
 41 | 					/** The end position (zero-based) of the recognized user's profile website. This end index is exclusive. */
 42 | 					end: number;
 43 | 					/** The URL in the format entered by the user. */
 44 | 					url: string;
 45 | 					/** The fully resolved URL. */
 46 | 					expanded_url: string;
 47 | 					/** The URL as displayed in the user's profile. */
 48 | 					display_url: string;
 49 | 				}>;
 50 | 			};
 51 | 			/** Contains details about URLs, Hashtags, Cashtags, or mentions located within a user's description. */
 52 | 			description: {
 53 | 				hashtags: Array<{
 54 | 					start: number;
 55 | 					end: number;
 56 | 					tag: string;
 57 | 				}>;
 58 | 			};
 59 | 		};
 60 | 		/**
 61 | 		 * Indicate if this user is a verified Twitter user.
 62 | 		 *
 63 | 		 * To return this field, add `user.fields=verified` in the authorization request's query parameter.
 64 | 		 */
 65 | 		verified?: boolean;
 66 | 		/**
 67 | 		 * The text of this user's profile description (also known as bio), if the user provided one.
 68 | 		 *
 69 | 		 * To return this field, add `user.fields=description` in the authorization request's query parameter.
 70 | 		 */
 71 | 		description?: string;
 72 | 		/**
 73 | 		 * The URL specified in the user's profile, if present.
 74 | 		 *
 75 | 		 * To return this field, add `user.fields=url` in the authorization request's query parameter.
 76 | 		 */
 77 | 		url?: string;
 78 | 		/** The URL to the profile image for this user, as shown on the user's profile. */
 79 | 		profile_image_url?: string;
 80 | 		protected?: boolean;
 81 | 		/**
 82 | 		 * Unique identifier of this user's pinned Tweet.
 83 | 		 *
 84 | 		 *  You can obtain the expanded object in `includes.tweets` by adding `expansions=pinned_tweet_id` in the authorization request's query parameter.
 85 | 		 */
 86 | 		pinned_tweet_id?: string;
 87 | 		created_at?: string;
 88 | 	};
 89 | 	includes?: {
 90 | 		tweets?: Array<{
 91 | 			id: string;
 92 | 			text: string;
 93 | 		}>;
 94 | 	};
 95 | 	[claims: string]: unknown;
 96 | }
 97 | 
 98 | export interface TwitterOption extends ProviderOptions<TwitterProfile> {
 99 | 	clientId: string;
100 | }
101 | 
102 | export const twitter = (options: TwitterOption) => {
103 | 	return {
104 | 		id: "twitter",
105 | 		name: "Twitter",
106 | 		createAuthorizationURL(data) {
107 | 			const _scopes = options.disableDefaultScope
108 | 				? []
109 | 				: ["users.read", "tweet.read", "offline.access", "users.email"];
110 | 			options.scope && _scopes.push(...options.scope);
111 | 			data.scopes && _scopes.push(...data.scopes);
112 | 			return createAuthorizationURL({
113 | 				id: "twitter",
114 | 				options,
115 | 				authorizationEndpoint: "https://x.com/i/oauth2/authorize",
116 | 				scopes: _scopes,
117 | 				state: data.state,
118 | 				codeVerifier: data.codeVerifier,
119 | 				redirectURI: data.redirectURI,
120 | 			});
121 | 		},
122 | 		validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
123 | 			return validateAuthorizationCode({
124 | 				code,
125 | 				codeVerifier,
126 | 				authentication: "basic",
127 | 				redirectURI,
128 | 				options,
129 | 				tokenEndpoint: "https://api.x.com/2/oauth2/token",
130 | 			});
131 | 		},
132 | 
133 | 		refreshAccessToken: options.refreshAccessToken
134 | 			? options.refreshAccessToken
135 | 			: async (refreshToken) => {
136 | 					return refreshAccessToken({
137 | 						refreshToken,
138 | 						options: {
139 | 							clientId: options.clientId,
140 | 							clientKey: options.clientKey,
141 | 							clientSecret: options.clientSecret,
142 | 						},
143 | 						authentication: "basic",
144 | 						tokenEndpoint: "https://api.x.com/2/oauth2/token",
145 | 					});
146 | 				},
147 | 		async getUserInfo(token) {
148 | 			if (options.getUserInfo) {
149 | 				return options.getUserInfo(token);
150 | 			}
151 | 			const { data: profile, error: profileError } =
152 | 				await betterFetch<TwitterProfile>(
153 | 					"https://api.x.com/2/users/me?user.fields=profile_image_url",
154 | 					{
155 | 						method: "GET",
156 | 						headers: {
157 | 							Authorization: `Bearer ${token.accessToken}`,
158 | 						},
159 | 					},
160 | 				);
161 | 
162 | 			if (profileError) {
163 | 				return null;
164 | 			}
165 | 
166 | 			const { data: emailData, error: emailError } = await betterFetch<{
167 | 				data: { confirmed_email: string };
168 | 			}>("https://api.x.com/2/users/me?user.fields=confirmed_email", {
169 | 				method: "GET",
170 | 				headers: {
171 | 					Authorization: `Bearer ${token.accessToken}`,
172 | 				},
173 | 			});
174 | 			let emailVerified = false;
175 | 			if (!emailError && emailData?.data?.confirmed_email) {
176 | 				profile.data.email = emailData.data.confirmed_email;
177 | 				emailVerified = true;
178 | 			}
179 | 			const userMap = await options.mapProfileToUser?.(profile);
180 | 			return {
181 | 				user: {
182 | 					id: profile.data.id,
183 | 					name: profile.data.name,
184 | 					email: profile.data.email || profile.data.username || null,
185 | 					image: profile.data.profile_image_url,
186 | 					emailVerified: emailVerified,
187 | 					...userMap,
188 | 				},
189 | 				data: profile,
190 | 			};
191 | 		},
192 | 		options,
193 | 	} satisfies OAuthProvider<TwitterProfile>;
194 | };
195 | 
```

--------------------------------------------------------------------------------
/docs/components/nav-mobile.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | import { ChevronRight, Menu } from "lucide-react";
  3 | import Link from "next/link";
  4 | import { Fragment, createContext, useContext, useState } from "react";
  5 | import {
  6 | 	Accordion,
  7 | 	AccordionContent,
  8 | 	AccordionItem,
  9 | 	AccordionTrigger,
 10 | } from "@/components/ui/accordion";
 11 | import { contents, examples } from "./sidebar-content";
 12 | import { usePathname } from "next/navigation";
 13 | import { cn } from "@/lib/utils";
 14 | 
 15 | interface NavbarMobileContextProps {
 16 | 	isOpen: boolean;
 17 | 	toggleNavbar: () => void;
 18 | 	isDocsOpen: boolean;
 19 | 	toggleDocsNavbar: () => void;
 20 | }
 21 | 
 22 | const NavbarContext = createContext<NavbarMobileContextProps | undefined>(
 23 | 	undefined,
 24 | );
 25 | 
 26 | export const NavbarProvider = ({ children }: { children: React.ReactNode }) => {
 27 | 	const [isOpen, setIsOpen] = useState(false);
 28 | 	const [isDocsOpen, setIsDocsOpen] = useState(false);
 29 | 
 30 | 	const toggleNavbar = () => {
 31 | 		setIsOpen((prevIsOpen) => !prevIsOpen);
 32 | 	};
 33 | 	const toggleDocsNavbar = () => {
 34 | 		setIsDocsOpen((prevIsOpen) => !prevIsOpen);
 35 | 	};
 36 | 	return (
 37 | 		<NavbarContext.Provider
 38 | 			value={{ isOpen, toggleNavbar, isDocsOpen, toggleDocsNavbar }}
 39 | 		>
 40 | 			{children}
 41 | 		</NavbarContext.Provider>
 42 | 	);
 43 | };
 44 | 
 45 | export const useNavbarMobile = (): NavbarMobileContextProps => {
 46 | 	const context = useContext(NavbarContext);
 47 | 	if (!context) {
 48 | 		throw new Error(
 49 | 			"useNavbarMobile must be used within a NavbarMobileProvider",
 50 | 		);
 51 | 	}
 52 | 	return context;
 53 | };
 54 | 
 55 | export const NavbarMobileBtn: React.FC = () => {
 56 | 	const { toggleNavbar } = useNavbarMobile();
 57 | 
 58 | 	return (
 59 | 		<div className="flex items-center">
 60 | 			<button
 61 | 				className="overflow-hidden px-2.5 block md:hidden"
 62 | 				onClick={() => {
 63 | 					toggleNavbar();
 64 | 				}}
 65 | 			>
 66 | 				<Menu className="size-5" />
 67 | 			</button>
 68 | 		</div>
 69 | 	);
 70 | };
 71 | 
 72 | export const NavbarMobile = () => {
 73 | 	const { isOpen, toggleNavbar } = useNavbarMobile();
 74 | 	const pathname = usePathname();
 75 | 	const isDocs = pathname.startsWith("/docs");
 76 | 
 77 | 	return (
 78 | 		<div
 79 | 			className={cn(
 80 | 				"fixed top-[50px] inset-x-0 transform-gpu z-[100] bg-background grid grid-rows-[0fr] duration-300 transition-all md:hidden",
 81 | 				isOpen &&
 82 | 					"shadow-lg border-b border-[rgba(255,255,255,.1)] grid-rows-[1fr]",
 83 | 			)}
 84 | 		>
 85 | 			<div
 86 | 				className={cn(
 87 | 					"px-9 min-h-0 overflow-y-auto max-h-[80vh] divide-y [mask-image:linear-gradient(to_top,transparent,white_40px)] transition-all duration-300",
 88 | 					isOpen ? "py-5" : "invisible",
 89 | 					isDocs && "px-4",
 90 | 				)}
 91 | 			>
 92 | 				{navMenu.map((menu) => (
 93 | 					<Fragment key={menu.name}>
 94 | 						{menu.child ? (
 95 | 							<Accordion type="single" collapsible>
 96 | 								<AccordionItem value={menu.name}>
 97 | 									<AccordionTrigger
 98 | 										className={cn(
 99 | 											"font-normal text-foreground",
100 | 											!isDocs && "text-2xl",
101 | 										)}
102 | 									>
103 | 										{menu.name}
104 | 									</AccordionTrigger>
105 | 									<AccordionContent className="pl-5 divide-y">
106 | 										{menu.child.map((child, j) => (
107 | 											<Link
108 | 												href={child.path}
109 | 												key={child.name}
110 | 												className={cn(
111 | 													"block py-2 border-b first:pt-0 last:pb-0 last:border-0 text-muted-foreground",
112 | 													!isDocs && "text-xl",
113 | 												)}
114 | 												onClick={toggleNavbar}
115 | 											>
116 | 												{child.name}
117 | 											</Link>
118 | 										))}
119 | 									</AccordionContent>
120 | 								</AccordionItem>
121 | 							</Accordion>
122 | 						) : (
123 | 							<Link
124 | 								href={menu.path}
125 | 								className={cn(
126 | 									"group flex items-center gap-2.5 first:pt-0 last:pb-0 text-2xl py-4",
127 | 									isDocs && "text-base py-2",
128 | 								)}
129 | 								onClick={toggleNavbar}
130 | 							>
131 | 								{isDocs && (
132 | 									<ChevronRight className="ml-0.5 size-4 text-muted-foreground md:hidden" />
133 | 								)}
134 | 								{menu.name}
135 | 							</Link>
136 | 						)}
137 | 					</Fragment>
138 | 				))}
139 | 				<DocsNavBarContent />
140 | 			</div>
141 | 		</div>
142 | 	);
143 | };
144 | 
145 | function DocsNavBarContent() {
146 | 	const pathname = usePathname();
147 | 	const { toggleNavbar } = useNavbarMobile();
148 | 	if (!pathname.startsWith("/docs")) return null;
149 | 
150 | 	const content = pathname.startsWith("/docs/examples") ? examples : contents;
151 | 
152 | 	return (
153 | 		<>
154 | 			{content.map((menu) => (
155 | 				<Accordion type="single" collapsible key={menu.title}>
156 | 					<AccordionItem value={menu.title}>
157 | 						<AccordionTrigger className="font-normal text-foreground">
158 | 							<div className="flex items-center gap-2">
159 | 								{!!menu.Icon && <menu.Icon className="w-5 h-5" />}
160 | 								{menu.title}
161 | 							</div>
162 | 						</AccordionTrigger>
163 | 						<AccordionContent className="pl-5 divide-y">
164 | 							{menu.list.map((child, index) =>
165 | 								child.group ? (
166 | 									// Group header rendered as div (just a divider)
167 | 									<div
168 | 										key={child.title}
169 | 										className="block py-2 text-sm text-muted-foreground border-none select-none"
170 | 									>
171 | 										<div className="flex flex-row items-center gap-2">
172 | 											<p className="text-sm text-primary">{child.title}</p>
173 | 											<div className="flex-grow h-px bg-border" />
174 | 										</div>
175 | 									</div>
176 | 								) : (
177 | 									// Regular menu item rendered as Link
178 | 									<Link
179 | 										href={child.href}
180 | 										key={child.title}
181 | 										className={`block py-2 text-sm text-muted-foreground ${
182 | 											// Add border only when not last item
183 | 											// and next item is not a group header
184 | 											index === menu.list.length - 1 ||
185 | 											menu.list[index + 1]?.group
186 | 												? "border-none"
187 | 												: "border-b"
188 | 										}`}
189 | 										onClick={toggleNavbar}
190 | 									>
191 | 										<div className="flex items-center gap-2">
192 | 											<child.icon />
193 | 											{child.title}
194 | 										</div>
195 | 									</Link>
196 | 								),
197 | 							)}
198 | 						</AccordionContent>
199 | 					</AccordionItem>
200 | 				</Accordion>
201 | 			))}
202 | 		</>
203 | 	);
204 | }
205 | 
206 | export const navMenu: {
207 | 	name: string;
208 | 	path: string;
209 | 	child?: {
210 | 		name: string;
211 | 		path: string;
212 | 	}[];
213 | }[] = [
214 | 	{
215 | 		name: "_helo",
216 | 		path: "/",
217 | 	},
218 | 
219 | 	{
220 | 		name: "docs",
221 | 		path: "/docs",
222 | 	},
223 | 	{
224 | 		name: "examples",
225 | 		path: "/docs/examples/next-js",
226 | 	},
227 | 	{
228 | 		name: "changelogs",
229 | 		path: "/changelogs",
230 | 	},
231 | 	{
232 | 		name: "blogs",
233 | 		path: "/blog",
234 | 	},
235 | 	{
236 | 		name: "community",
237 | 		path: "/community",
238 | 	},
239 | ];
240 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/utils/wildcard.ts:
--------------------------------------------------------------------------------

```typescript
  1 | //https://github.com/axtgr/wildcard-match
  2 | 
  3 | /**
  4 |  * Escapes a character if it has a special meaning in regular expressions
  5 |  * and returns the character as is if it doesn't
  6 |  */
  7 | function escapeRegExpChar(char: string) {
  8 | 	if (
  9 | 		char === "-" ||
 10 | 		char === "^" ||
 11 | 		char === "$" ||
 12 | 		char === "+" ||
 13 | 		char === "." ||
 14 | 		char === "(" ||
 15 | 		char === ")" ||
 16 | 		char === "|" ||
 17 | 		char === "[" ||
 18 | 		char === "]" ||
 19 | 		char === "{" ||
 20 | 		char === "}" ||
 21 | 		char === "*" ||
 22 | 		char === "?" ||
 23 | 		char === "\\"
 24 | 	) {
 25 | 		return `\\${char}`;
 26 | 	} else {
 27 | 		return char;
 28 | 	}
 29 | }
 30 | 
 31 | /**
 32 |  * Escapes all characters in a given string that have a special meaning in regular expressions
 33 |  */
 34 | function escapeRegExpString(str: string) {
 35 | 	let result = "";
 36 | 	for (let i = 0; i < str.length; i++) {
 37 | 		result += escapeRegExpChar(str[i]!);
 38 | 	}
 39 | 	return result;
 40 | }
 41 | 
 42 | /**
 43 |  * Transforms one or more glob patterns into a RegExp pattern
 44 |  */
 45 | function transform(
 46 | 	pattern: string | string[],
 47 | 	separator: string | boolean = true,
 48 | ): string {
 49 | 	if (Array.isArray(pattern)) {
 50 | 		let regExpPatterns = pattern.map((p) => `^${transform(p, separator)}$`);
 51 | 		return `(?:${regExpPatterns.join("|")})`;
 52 | 	}
 53 | 
 54 | 	let separatorSplitter = "";
 55 | 	let separatorMatcher = "";
 56 | 	let wildcard = ".";
 57 | 
 58 | 	if (separator === true) {
 59 | 		// In this case forward slashes in patterns match both forward and backslashes in samples:
 60 | 		//
 61 | 		// `foo/bar` will match `foo/bar`
 62 | 		//           will match `foo\bar`
 63 | 		//
 64 | 		separatorSplitter = "/";
 65 | 		separatorMatcher = "[/\\\\]";
 66 | 		wildcard = "[^/\\\\]";
 67 | 	} else if (separator) {
 68 | 		separatorSplitter = separator;
 69 | 		separatorMatcher = escapeRegExpString(separatorSplitter);
 70 | 
 71 | 		if (separatorMatcher.length > 1) {
 72 | 			separatorMatcher = `(?:${separatorMatcher})`;
 73 | 			wildcard = `((?!${separatorMatcher}).)`;
 74 | 		} else {
 75 | 			wildcard = `[^${separatorMatcher}]`;
 76 | 		}
 77 | 	}
 78 | 
 79 | 	// When a separator is explicitly specified in a pattern,
 80 | 	// it MUST match ONE OR MORE separators in a sample:
 81 | 	//
 82 | 	// `foo/bar/` will match  `foo//bar///`
 83 | 	//            won't match `foo/bar`
 84 | 	//
 85 | 	// When a pattern doesn't have a trailing separator,
 86 | 	// a sample can still optionally have them:
 87 | 	//
 88 | 	// `foo/bar` will match `foo/bar//`
 89 | 	//
 90 | 	// So we use different quantifiers depending on the index of a segment.
 91 | 	let requiredSeparator = separator ? `${separatorMatcher}+?` : "";
 92 | 	let optionalSeparator = separator ? `${separatorMatcher}*?` : "";
 93 | 
 94 | 	let segments = separator ? pattern.split(separatorSplitter) : [pattern];
 95 | 	let result = "";
 96 | 
 97 | 	for (let s = 0; s < segments.length; s++) {
 98 | 		let segment = segments[s]!;
 99 | 		let nextSegment = segments[s + 1]!;
100 | 		let currentSeparator = "";
101 | 
102 | 		if (!segment && s > 0) {
103 | 			continue;
104 | 		}
105 | 
106 | 		if (separator) {
107 | 			if (s === segments.length - 1) {
108 | 				currentSeparator = optionalSeparator;
109 | 			} else if (nextSegment !== "**") {
110 | 				currentSeparator = requiredSeparator;
111 | 			} else {
112 | 				currentSeparator = "";
113 | 			}
114 | 		}
115 | 
116 | 		if (separator && segment === "**") {
117 | 			if (currentSeparator) {
118 | 				result += s === 0 ? "" : currentSeparator;
119 | 				result += `(?:${wildcard}*?${currentSeparator})*?`;
120 | 			}
121 | 			continue;
122 | 		}
123 | 
124 | 		for (let c = 0; c < segment.length; c++) {
125 | 			let char = segment[c]!;
126 | 
127 | 			if (char === "\\") {
128 | 				if (c < segment.length - 1) {
129 | 					result += escapeRegExpChar(segment[c + 1]!);
130 | 					c++;
131 | 				}
132 | 			} else if (char === "?") {
133 | 				result += wildcard;
134 | 			} else if (char === "*") {
135 | 				result += `${wildcard}*?`;
136 | 			} else {
137 | 				result += escapeRegExpChar(char);
138 | 			}
139 | 		}
140 | 
141 | 		result += currentSeparator;
142 | 	}
143 | 
144 | 	return result;
145 | }
146 | 
147 | export default transform;
148 | 
149 | interface WildcardMatchOptions {
150 | 	/** Separator to be used to split patterns and samples into segments */
151 | 	separator?: string | boolean;
152 | 
153 | 	/** Flags to pass to the RegExp */
154 | 	flags?: string;
155 | }
156 | 
157 | // This overrides the function's signature because for the end user
158 | // the function is always bound to a RegExp
159 | interface isMatch {
160 | 	/**
161 | 	 * Tests if a sample string matches the pattern(s)
162 | 	 *
163 | 	 * ```js
164 | 	 * isMatch('foo') //=> true
165 | 	 * ```
166 | 	 */
167 | 	(sample: string): boolean;
168 | 
169 | 	/** Compiled regular expression */
170 | 	regexp: RegExp;
171 | 
172 | 	/** Original pattern or array of patterns that was used to compile the RegExp */
173 | 	pattern: string | string[];
174 | 
175 | 	/** Options that were used to compile the RegExp */
176 | 	options: WildcardMatchOptions;
177 | }
178 | 
179 | function isMatch(regexp: RegExp, sample: string) {
180 | 	if (typeof sample !== "string") {
181 | 		throw new TypeError(`Sample must be a string, but ${typeof sample} given`);
182 | 	}
183 | 
184 | 	return regexp.test(sample);
185 | }
186 | 
187 | /**
188 |  * Compiles one or more glob patterns into a RegExp and returns an isMatch function.
189 |  * The isMatch function takes a sample string as its only argument and returns `true`
190 |  * if the string matches the pattern(s).
191 |  *
192 |  * ```js
193 |  * wildcardMatch('src/*.js')('src/index.js') //=> true
194 |  * ```
195 |  *
196 |  * ```js
197 |  * const isMatch = wildcardMatch('*.example.com', '.')
198 |  * isMatch('foo.example.com') //=> true
199 |  * isMatch('foo.bar.com') //=> false
200 |  * ```
201 |  */
202 | function wildcardMatch(
203 | 	pattern: string | string[],
204 | 	options?: string | boolean | WildcardMatchOptions,
205 | ) {
206 | 	if (typeof pattern !== "string" && !Array.isArray(pattern)) {
207 | 		throw new TypeError(
208 | 			`The first argument must be a single pattern string or an array of patterns, but ${typeof pattern} given`,
209 | 		);
210 | 	}
211 | 
212 | 	if (typeof options === "string" || typeof options === "boolean") {
213 | 		options = { separator: options };
214 | 	}
215 | 
216 | 	if (
217 | 		arguments.length === 2 &&
218 | 		!(
219 | 			typeof options === "undefined" ||
220 | 			(typeof options === "object" &&
221 | 				options !== null &&
222 | 				!Array.isArray(options))
223 | 		)
224 | 	) {
225 | 		throw new TypeError(
226 | 			`The second argument must be an options object or a string/boolean separator, but ${typeof options} given`,
227 | 		);
228 | 	}
229 | 
230 | 	options = options || {};
231 | 
232 | 	if (options.separator === "\\") {
233 | 		throw new Error(
234 | 			"\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead",
235 | 		);
236 | 	}
237 | 
238 | 	let regexpPattern = transform(pattern, options.separator);
239 | 	let regexp = new RegExp(`^${regexpPattern}$`, options.flags);
240 | 
241 | 	let fn = isMatch.bind(null, regexp) as isMatch;
242 | 	fn.options = options;
243 | 	fn.pattern = pattern;
244 | 	fn.regexp = regexp;
245 | 	return fn;
246 | }
247 | 
248 | export { wildcardMatch, isMatch };
249 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/anonymous/anon.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 | 	describe,
  3 | 	expect,
  4 | 	it,
  5 | 	vi,
  6 | 	beforeAll,
  7 | 	afterAll,
  8 | 	afterEach,
  9 | } from "vitest";
 10 | import { setupServer } from "msw/node";
 11 | import { http, HttpResponse } from "msw";
 12 | import { anonymous } from ".";
 13 | import { getTestInstance } from "../../test-utils/test-instance";
 14 | import { createAuthClient } from "../../client";
 15 | import { anonymousClient } from "./client";
 16 | import type { GoogleProfile } from "@better-auth/core/social-providers";
 17 | import { DEFAULT_SECRET } from "../../utils/constants";
 18 | import { signJWT } from "../../crypto";
 19 | 
 20 | let testIdToken: string;
 21 | let handlers: ReturnType<typeof http.post>[];
 22 | 
 23 | const server = setupServer();
 24 | 
 25 | beforeAll(async () => {
 26 | 	const data: GoogleProfile = {
 27 | 		email: "[email protected]",
 28 | 		email_verified: true,
 29 | 		name: "First Last",
 30 | 		picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw",
 31 | 		exp: 1234567890,
 32 | 		sub: "1234567890",
 33 | 		iat: 1234567890,
 34 | 		aud: "test",
 35 | 		azp: "test",
 36 | 		nbf: 1234567890,
 37 | 		iss: "test",
 38 | 		locale: "en",
 39 | 		jti: "test",
 40 | 		given_name: "First",
 41 | 		family_name: "Last",
 42 | 	};
 43 | 	testIdToken = await signJWT(data, DEFAULT_SECRET);
 44 | 
 45 | 	handlers = [
 46 | 		http.post("https://oauth2.googleapis.com/token", () => {
 47 | 			return HttpResponse.json({
 48 | 				access_token: "test",
 49 | 				refresh_token: "test",
 50 | 				id_token: testIdToken,
 51 | 			});
 52 | 		}),
 53 | 	];
 54 | 
 55 | 	server.listen({ onUnhandledRequest: "bypass" });
 56 | 	server.use(...handlers);
 57 | });
 58 | 
 59 | afterEach(() => {
 60 | 	server.resetHandlers();
 61 | 	server.use(...handlers);
 62 | });
 63 | 
 64 | afterAll(() => server.close());
 65 | 
 66 | describe("anonymous", async () => {
 67 | 	const linkAccountFn = vi.fn();
 68 | 	const { customFetchImpl, auth, sessionSetter, testUser, cookieSetter } =
 69 | 		await getTestInstance({
 70 | 			plugins: [
 71 | 				anonymous({
 72 | 					async onLinkAccount(data) {
 73 | 						linkAccountFn(data);
 74 | 					},
 75 | 					schema: {
 76 | 						user: {
 77 | 							fields: {
 78 | 								isAnonymous: "is_anon",
 79 | 							},
 80 | 						},
 81 | 					},
 82 | 				}),
 83 | 			],
 84 | 			socialProviders: {
 85 | 				google: {
 86 | 					clientId: "test",
 87 | 					clientSecret: "test",
 88 | 				},
 89 | 			},
 90 | 		});
 91 | 	const headers = new Headers();
 92 | 	const client = createAuthClient({
 93 | 		plugins: [anonymousClient()],
 94 | 		fetchOptions: {
 95 | 			customFetchImpl,
 96 | 		},
 97 | 		baseURL: "http://localhost:3000",
 98 | 	});
 99 | 
100 | 	it("should sign in anonymously", async () => {
101 | 		await client.signIn.anonymous({
102 | 			fetchOptions: {
103 | 				onSuccess: sessionSetter(headers),
104 | 			},
105 | 		});
106 | 		const session = await client.getSession({
107 | 			fetchOptions: {
108 | 				headers,
109 | 			},
110 | 		});
111 | 		expect(session.data?.session).toBeDefined();
112 | 		expect(session.data?.user.isAnonymous).toBe(true);
113 | 	});
114 | 
115 | 	it("link anonymous user account", async () => {
116 | 		expect(linkAccountFn).toHaveBeenCalledTimes(0);
117 | 		const res = await client.signIn.email(testUser, {
118 | 			headers,
119 | 		});
120 | 		expect(linkAccountFn).toHaveBeenCalledWith(expect.any(Object));
121 | 		linkAccountFn.mockClear();
122 | 	});
123 | 
124 | 	it("should link in social sign on", async () => {
125 | 		const headers = new Headers();
126 | 		await client.signIn.anonymous({
127 | 			fetchOptions: {
128 | 				onSuccess: sessionSetter(headers),
129 | 			},
130 | 		});
131 | 
132 | 		await client.getSession({
133 | 			fetchOptions: {
134 | 				headers,
135 | 			},
136 | 		});
137 | 
138 | 		const singInRes = await client.signIn.social({
139 | 			provider: "google",
140 | 			callbackURL: "/dashboard",
141 | 			fetchOptions: {
142 | 				onSuccess: cookieSetter(headers),
143 | 			},
144 | 		});
145 | 		const state = new URL(singInRes.data?.url || "").searchParams.get("state");
146 | 		await client.$fetch("/callback/google", {
147 | 			query: {
148 | 				state,
149 | 				code: "test",
150 | 			},
151 | 			headers,
152 | 		});
153 | 		expect(linkAccountFn).toHaveBeenCalledWith(expect.any(Object));
154 | 	});
155 | 
156 | 	it("should work with generateName", async () => {
157 | 		const { customFetchImpl, sessionSetter } = await getTestInstance({
158 | 			plugins: [
159 | 				anonymous({
160 | 					generateName() {
161 | 						return "i-am-anonymous";
162 | 					},
163 | 				}),
164 | 			],
165 | 		});
166 | 		const client = createAuthClient({
167 | 			plugins: [anonymousClient()],
168 | 			fetchOptions: {
169 | 				customFetchImpl,
170 | 			},
171 | 			baseURL: "http://localhost:3000",
172 | 		});
173 | 		const res = await client.signIn.anonymous({
174 | 			fetchOptions: {
175 | 				onSuccess: sessionSetter(headers),
176 | 			},
177 | 		});
178 | 		expect(res.data?.user.name).toBe("i-am-anonymous");
179 | 	});
180 | 
181 | 	it("should not reject first-time anonymous sign-in", async () => {
182 | 		const { customFetchImpl, sessionSetter } = await getTestInstance({
183 | 			plugins: [anonymous()],
184 | 		});
185 | 		const client = createAuthClient({
186 | 			plugins: [anonymousClient()],
187 | 			fetchOptions: {
188 | 				customFetchImpl,
189 | 			},
190 | 			baseURL: "http://localhost:3000",
191 | 		});
192 | 		const freshHeaders = new Headers();
193 | 
194 | 		// First-time anonymous sign-in should succeed without 400 error
195 | 		const res = await client.signIn.anonymous({
196 | 			fetchOptions: {
197 | 				onSuccess: sessionSetter(freshHeaders),
198 | 			},
199 | 		});
200 | 
201 | 		expect(res.data?.user).toBeDefined();
202 | 		expect(res.error).toBeNull();
203 | 
204 | 		// Verify session is actually created and contains isAnonymous
205 | 		const session = await client.getSession({
206 | 			fetchOptions: {
207 | 				headers: freshHeaders,
208 | 			},
209 | 		});
210 | 		expect(session.data?.session).toBeDefined();
211 | 		expect(session.data?.user.isAnonymous).toBe(true);
212 | 	});
213 | 
214 | 	it("should reject subsequent anonymous sign-in attempts once signed in", async () => {
215 | 		const { customFetchImpl, sessionSetter } = await getTestInstance({
216 | 			plugins: [anonymous()],
217 | 		});
218 | 		const client = createAuthClient({
219 | 			plugins: [anonymousClient()],
220 | 			fetchOptions: {
221 | 				customFetchImpl,
222 | 			},
223 | 			baseURL: "http://localhost:3000",
224 | 		});
225 | 		const persistentHeaders = new Headers();
226 | 
227 | 		// First sign-in should succeed
228 | 		await client.signIn.anonymous({
229 | 			fetchOptions: {
230 | 				headers: persistentHeaders,
231 | 				onSuccess: sessionSetter(persistentHeaders),
232 | 			},
233 | 		});
234 | 
235 | 		// Verify session is established before testing rejection
236 | 		const session = await client.getSession({
237 | 			fetchOptions: {
238 | 				headers: persistentHeaders,
239 | 			},
240 | 		});
241 | 		expect(session.data?.session).toBeDefined();
242 | 		expect(session.data?.user.isAnonymous).toBe(true);
243 | 
244 | 		// Second attempt should be rejected at the endpoint level
245 | 		const secondAttempt = await client.signIn.anonymous({
246 | 			fetchOptions: {
247 | 				headers: persistentHeaders,
248 | 			},
249 | 		});
250 | 
251 | 		expect(secondAttempt.data).toBeNull();
252 | 		expect(secondAttempt.error).toBeDefined();
253 | 		expect(secondAttempt.error?.message).toBe(
254 | 			"Anonymous users cannot sign in again anonymously",
255 | 		);
256 | 	});
257 | });
258 | 
```

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

```typescript
  1 | import { ImageResponse } from "@vercel/og";
  2 | import { z } from "zod";
  3 | export const runtime = "edge";
  4 | 
  5 | const ogSchema = z.object({
  6 | 	heading: z.string(),
  7 | 	mode: z.string(),
  8 | 	type: z.string(),
  9 | });
 10 | export async function GET(req: Request) {
 11 | 	try {
 12 | 		const geist = await fetch(
 13 | 			new URL("../../../assets/Geist.ttf", import.meta.url),
 14 | 		).then((res) => res.arrayBuffer());
 15 | 		const geistMono = await fetch(
 16 | 			new URL("../../../assets/GeistMono.ttf", import.meta.url),
 17 | 		).then((res) => res.arrayBuffer());
 18 | 		const url = new URL(req.url);
 19 | 		const urlParamsValues = Object.fromEntries(url.searchParams);
 20 | 		const validParams = ogSchema.parse(urlParamsValues);
 21 | 		const { heading, type } = validParams;
 22 | 		const trueHeading =
 23 | 			heading.length > 140 ? `${heading.substring(0, 140)}...` : heading;
 24 | 
 25 | 		const paint = "#fff";
 26 | 
 27 | 		const fontSize = trueHeading.length > 100 ? "30px" : "60px";
 28 | 		return new ImageResponse(
 29 | 			<div
 30 | 				tw="flex w-full relative flex-col p-12"
 31 | 				style={{
 32 | 					color: paint,
 33 | 					backgroundColor: "transparent",
 34 | 					border: "1px solid rgba(255, 255, 255, 0.1)",
 35 | 					boxShadow: "0 -20px 80px -20px rgba(28, 12, 12, 0.1) inset",
 36 | 					background: "#0a0505",
 37 | 				}}
 38 | 			>
 39 | 				<div
 40 | 					tw={`relative flex flex-col w-full h-full border-2 border-[${paint}]/20 p-10}`}
 41 | 				>
 42 | 					<svg
 43 | 						style={{
 44 | 							position: "absolute",
 45 | 							top: "-9px",
 46 | 							right: "-9px",
 47 | 						}}
 48 | 						width="17"
 49 | 						height="17"
 50 | 						fill="none"
 51 | 					>
 52 | 						<path
 53 | 							d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
 54 | 							fill="#d0cfd1d3"
 55 | 						/>
 56 | 					</svg>
 57 | 
 58 | 					<svg
 59 | 						style={{
 60 | 							position: "absolute",
 61 | 							top: "-9px",
 62 | 							left: "-9px",
 63 | 						}}
 64 | 						width="17"
 65 | 						height="17"
 66 | 						fill="none"
 67 | 					>
 68 | 						<path
 69 | 							d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
 70 | 							fill="#cacaca"
 71 | 						/>
 72 | 					</svg>
 73 | 					<svg
 74 | 						style={{
 75 | 							position: "absolute",
 76 | 							bottom: "-9px",
 77 | 							left: "-9px",
 78 | 						}}
 79 | 						width="17"
 80 | 						height="17"
 81 | 						fill="none"
 82 | 					>
 83 | 						<path
 84 | 							d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
 85 | 							fill="#cacaca"
 86 | 						/>
 87 | 					</svg>
 88 | 					<svg
 89 | 						style={{
 90 | 							position: "absolute",
 91 | 							bottom: "-9px",
 92 | 							right: "-9px",
 93 | 						}}
 94 | 						width="17"
 95 | 						height="17"
 96 | 						fill="none"
 97 | 					>
 98 | 						<path
 99 | 							d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
100 | 							fill="#cacaca"
101 | 						/>
102 | 					</svg>
103 | 					<div tw="flex flex-col flex-1 py-10">
104 | 						<svg
105 | 							width="100"
106 | 							height="95"
107 | 							viewBox="0 0 60 45"
108 | 							fill="none"
109 | 							className="mb-10"
110 | 							xmlns="http://www.w3.org/2000/svg"
111 | 						>
112 | 							<path
113 | 								fillRule="evenodd"
114 | 								stroke={paint}
115 | 								clipRule="evenodd"
116 | 								d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z"
117 | 								fill="white"
118 | 							/>
119 | 						</svg>
120 | 						<div
121 | 							style={{ fontFamily: "GeistMono", fontWeight: "normal" }}
122 | 							tw="relative flex mt-10 text-xl uppercase font-bold gap-2 items-center"
123 | 						>
124 | 							{type === "documentation" ? (
125 | 								<svg
126 | 									xmlns="http://www.w3.org/2000/svg"
127 | 									width="1.2em"
128 | 									height="1.2em"
129 | 									viewBox="0 0 24 24"
130 | 								>
131 | 									<path
132 | 										fill="currentColor"
133 | 										fillRule="evenodd"
134 | 										d="M4.172 3.172C3 4.343 3 6.229 3 10v4c0 3.771 0 5.657 1.172 6.828S7.229 22 11 22h2c3.771 0 5.657 0 6.828-1.172S21 17.771 21 14v-4c0-3.771 0-5.657-1.172-6.828S16.771 2 13 2h-2C7.229 2 5.343 2 4.172 3.172M8 9.25a.75.75 0 0 0 0 1.5h8a.75.75 0 0 0 0-1.5zm0 4a.75.75 0 0 0 0 1.5h5a.75.75 0 0 0 0-1.5z"
135 | 										clipRule="evenodd"
136 | 									></path>
137 | 								</svg>
138 | 							) : null}
139 | 							{type}
140 | 						</div>
141 | 						<div
142 | 							tw="flex max-w-[70%] mt-5 tracking-tighter leading-[1.1] text-[30px] font-bold"
143 | 							style={{
144 | 								fontWeight: "bold",
145 | 								marginLeft: "-3px",
146 | 								fontSize,
147 | 
148 | 								fontFamily: "GeistMono",
149 | 							}}
150 | 						>
151 | 							{trueHeading}
152 | 						</div>
153 | 					</div>
154 | 					<div tw="flex items-center w-full justify-between">
155 | 						<div
156 | 							tw="flex text-xl"
157 | 							style={{ fontFamily: "GeistSans", fontWeight: "semibold" }}
158 | 						>
159 | 							Better Auth.
160 | 						</div>
161 | 						<div tw="flex gap-2 items-center text-xl">
162 | 							<svg
163 | 								xmlns="http://www.w3.org/2000/svg"
164 | 								width="1.2em"
165 | 								height="1.2em"
166 | 								viewBox="0 0 24 24"
167 | 							>
168 | 								<path
169 | 									fill="currentColor"
170 | 									d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
171 | 								></path>
172 | 							</svg>
173 | 							<span
174 | 								style={{
175 | 									fontFamily: "GeistSans",
176 | 								}}
177 | 								tw="flex ml-2"
178 | 							>
179 | 								github.com/better-auth/better-auth
180 | 							</span>
181 | 						</div>
182 | 					</div>
183 | 				</div>
184 | 			</div>,
185 | 			{
186 | 				width: 1200,
187 | 				height: 630,
188 | 				fonts: [
189 | 					{
190 | 						name: "Geist",
191 | 						data: geist,
192 | 						weight: 400,
193 | 						style: "normal",
194 | 					},
195 | 					{
196 | 						name: "GeistMono",
197 | 						data: geistMono,
198 | 						weight: 700,
199 | 						style: "normal",
200 | 					},
201 | 				],
202 | 			},
203 | 		);
204 | 	} catch (err) {
205 | 		console.log({ err });
206 | 		return new Response("Failed to generate the og image", { status: 500 });
207 | 	}
208 | }
209 | 
```

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

```typescript
  1 | import { afterAll, beforeAll, describe } from "vitest";
  2 | import type { BetterAuthOptions } from "@better-auth/core";
  3 | import type { DBAdapter } from "@better-auth/core/db/adapter";
  4 | import { getAuthTables } from "../db";
  5 | import type { createTestSuite } from "./create-test-suite";
  6 | import { TTY_COLORS } from "@better-auth/core/env";
  7 | import { deepmerge } from "./utils";
  8 | 
  9 | export type Logger = {
 10 | 	info: (...args: any[]) => void;
 11 | 	success: (...args: any[]) => void;
 12 | 	warn: (...args: any[]) => void;
 13 | 	error: (...args: any[]) => void;
 14 | 	debug: (...args: any[]) => void;
 15 | };
 16 | 
 17 | export const testAdapter = async ({
 18 | 	adapter: getAdapter,
 19 | 	runMigrations,
 20 | 	overrideBetterAuthOptions,
 21 | 	additionalCleanups,
 22 | 	tests,
 23 | 	prefixTests,
 24 | 	onFinish,
 25 | 	customIdGenerator,
 26 | }: {
 27 | 	/**
 28 | 	 * A function that will return the adapter instance to test with.
 29 | 	 *
 30 | 	 * @example
 31 | 	 * ```ts
 32 | 	 * testAdapter({
 33 | 	 *   adapter: (options) => drizzleAdapter(drizzle(db), {
 34 | 	 *     schema: generateSchema(options),
 35 | 	 *   }),
 36 | 	 * })
 37 | 	 */
 38 | 	adapter: (
 39 | 		options: BetterAuthOptions,
 40 | 	) =>
 41 | 		| Promise<(options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>>
 42 | 		| ((options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>);
 43 | 	/**
 44 | 	 * A function that will run the database migrations.
 45 | 	 */
 46 | 	runMigrations: (betterAuthOptions: BetterAuthOptions) => Promise<void> | void;
 47 | 	/**
 48 | 	 * Any potential better-auth options overrides.
 49 | 	 */
 50 | 	overrideBetterAuthOptions?: <
 51 | 		Passed extends BetterAuthOptions,
 52 | 		Returned extends BetterAuthOptions,
 53 | 	>(
 54 | 		betterAuthOptions: Passed,
 55 | 	) => Returned;
 56 | 	/**
 57 | 	 * By default we will cleanup all tables automatically,
 58 | 	 * but if you have additional cleanup logic, you can pass it here.
 59 | 	 *
 60 | 	 * Such as deleting a DB file that could had been created.
 61 | 	 */
 62 | 	additionalCleanups?: () => Promise<void> | void;
 63 | 	/**
 64 | 	 * A test suite to run.
 65 | 	 */
 66 | 	tests: ReturnType<ReturnType<typeof createTestSuite>>[];
 67 | 	/**
 68 | 	 * A prefix to add to the test suite name.
 69 | 	 */
 70 | 	prefixTests?: string;
 71 | 	/**
 72 | 	 * Upon finish of the tests, this function will be called.
 73 | 	 */
 74 | 	onFinish?: () => Promise<void> | void;
 75 | 	/**
 76 | 	 * Custom ID generator function to be used by the helper functions. (such as `insertRandom`)
 77 | 	 */
 78 | 	customIdGenerator?: () => string | Promise<string>;
 79 | }) => {
 80 | 	const defaultBAOptions = {} satisfies BetterAuthOptions;
 81 | 	let betterAuthOptions = (() => {
 82 | 		return {
 83 | 			...defaultBAOptions,
 84 | 			...(overrideBetterAuthOptions?.(defaultBAOptions) || {}),
 85 | 		} satisfies BetterAuthOptions;
 86 | 	})();
 87 | 
 88 | 	let adapter: DBAdapter<BetterAuthOptions> = (
 89 | 		await getAdapter(betterAuthOptions)
 90 | 	)(betterAuthOptions);
 91 | 
 92 | 	const adapterName = adapter.options?.adapterConfig.adapterName;
 93 | 	const adapterId = adapter.options?.adapterConfig.adapterId || adapter.id;
 94 | 	const adapterDisplayName = adapterName || adapterId;
 95 | 
 96 | 	const refreshAdapter = async (betterAuthOptions: BetterAuthOptions) => {
 97 | 		adapter = (await getAdapter(betterAuthOptions))(betterAuthOptions);
 98 | 	};
 99 | 
100 | 	/**
101 | 	 * A helper function to log to the console.
102 | 	 */
103 | 	const log: Logger = (() => {
104 | 		return {
105 | 			info: (...args: any[]) =>
106 | 				console.log(
107 | 					`${TTY_COLORS.fg.blue}INFO   ${TTY_COLORS.reset} [${adapterDisplayName}]`,
108 | 					...args,
109 | 				),
110 | 			success: (...args: any[]) =>
111 | 				console.log(
112 | 					`${TTY_COLORS.fg.green}SUCCESS${TTY_COLORS.reset} [${adapterDisplayName}]`,
113 | 					...args,
114 | 				),
115 | 			warn: (...args: any[]) =>
116 | 				console.log(
117 | 					`${TTY_COLORS.fg.yellow}WARN   ${TTY_COLORS.reset} [${adapterDisplayName}]`,
118 | 					...args,
119 | 				),
120 | 			error: (...args: any[]) =>
121 | 				console.log(
122 | 					`${TTY_COLORS.fg.red}ERROR  ${TTY_COLORS.reset} [${adapterDisplayName}]`,
123 | 					...args,
124 | 				),
125 | 			debug: (...args: any[]) =>
126 | 				console.log(
127 | 					`${TTY_COLORS.fg.magenta}DEBUG  ${TTY_COLORS.reset} [${adapterDisplayName}]`,
128 | 					...args,
129 | 				),
130 | 		};
131 | 	})();
132 | 
133 | 	/**
134 | 	 * Cleanup function to remove all rows from the database.
135 | 	 */
136 | 	const cleanup = async () => {
137 | 		const start = performance.now();
138 | 		await refreshAdapter(betterAuthOptions);
139 | 		const getAllModels = getAuthTables(betterAuthOptions);
140 | 
141 | 		// Clean up all rows from all models
142 | 		for (const model of Object.keys(getAllModels)) {
143 | 			try {
144 | 				await adapter.deleteMany({ model: model, where: [] });
145 | 			} catch (error) {
146 | 				const msg = `Error while cleaning up all rows from ${model}`;
147 | 				log.error(msg, error);
148 | 				throw new Error(msg, {
149 | 					cause: error,
150 | 				});
151 | 			}
152 | 		}
153 | 
154 | 		// Run additional cleanups
155 | 		try {
156 | 			await additionalCleanups?.();
157 | 		} catch (error) {
158 | 			const msg = `Error while running additional cleanups`;
159 | 			log.error(msg, error);
160 | 			throw new Error(msg, {
161 | 				cause: error,
162 | 			});
163 | 		}
164 | 		await refreshAdapter(betterAuthOptions);
165 | 		log.success(
166 | 			`${TTY_COLORS.bright}CLEAN-UP${TTY_COLORS.reset} completed successfully (${(performance.now() - start).toFixed(3)}ms)`,
167 | 		);
168 | 	};
169 | 
170 | 	/**
171 | 	 * A function that will run the database migrations.
172 | 	 */
173 | 	const migrate = async () => {
174 | 		const start = performance.now();
175 | 
176 | 		try {
177 | 			await runMigrations(betterAuthOptions);
178 | 		} catch (error) {
179 | 			const msg = `Error while running migrations`;
180 | 			log.error(msg, error);
181 | 			throw new Error(msg, {
182 | 				cause: error,
183 | 			});
184 | 		}
185 | 		log.success(
186 | 			`${TTY_COLORS.bright}MIGRATIONS${TTY_COLORS.reset} completed successfully (${(performance.now() - start).toFixed(3)}ms)`,
187 | 		);
188 | 	};
189 | 
190 | 	return {
191 | 		execute: () => {
192 | 			describe(adapterDisplayName, async () => {
193 | 				beforeAll(async () => {
194 | 					await migrate();
195 | 				}, 20000);
196 | 
197 | 				afterAll(async () => {
198 | 					await cleanup();
199 | 					await onFinish?.();
200 | 				}, 20000);
201 | 
202 | 				for (const testSuite of tests) {
203 | 					await testSuite({
204 | 						adapter: async () => {
205 | 							await refreshAdapter(betterAuthOptions);
206 | 							return adapter;
207 | 						},
208 | 						adapterDisplayName,
209 | 						log,
210 | 						getBetterAuthOptions: () => betterAuthOptions,
211 | 						modifyBetterAuthOptions: async (options) => {
212 | 							const newOptions = deepmerge(defaultBAOptions, options);
213 | 							betterAuthOptions = deepmerge(
214 | 								newOptions,
215 | 								overrideBetterAuthOptions?.(newOptions) || {},
216 | 							);
217 | 							await refreshAdapter(betterAuthOptions);
218 | 							return betterAuthOptions;
219 | 						},
220 | 						cleanup,
221 | 						prefixTests,
222 | 						runMigrations: migrate,
223 | 						onTestFinish: async () => {},
224 | 						customIdGenerator,
225 | 					});
226 | 				}
227 | 			});
228 | 		},
229 | 	};
230 | };
231 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
  2 | import { setupServer } from "msw/node";
  3 | import { http, HttpResponse } from "msw";
  4 | import { getTestInstance } from "../../test-utils/test-instance";
  5 | import { oAuthProxy } from ".";
  6 | import type { GoogleProfile } from "@better-auth/core/social-providers";
  7 | import { DEFAULT_SECRET } from "../../utils/constants";
  8 | import { signJWT } from "../../crypto";
  9 | 
 10 | let testIdToken: string;
 11 | let handlers: ReturnType<typeof http.post>[];
 12 | 
 13 | const server = setupServer();
 14 | 
 15 | beforeAll(async () => {
 16 | 	const data: GoogleProfile = {
 17 | 		email: "[email protected]",
 18 | 		email_verified: true,
 19 | 		name: "First Last",
 20 | 		picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw",
 21 | 		exp: 1234567890,
 22 | 		sub: "1234567890",
 23 | 		iat: 1234567890,
 24 | 		aud: "test",
 25 | 		azp: "test",
 26 | 		nbf: 1234567890,
 27 | 		iss: "test",
 28 | 		locale: "en",
 29 | 		jti: "test",
 30 | 		given_name: "First",
 31 | 		family_name: "Last",
 32 | 	};
 33 | 	testIdToken = await signJWT(data, DEFAULT_SECRET);
 34 | 
 35 | 	handlers = [
 36 | 		http.post("https://oauth2.googleapis.com/token", () => {
 37 | 			return HttpResponse.json({
 38 | 				access_token: "test",
 39 | 				refresh_token: "test",
 40 | 				id_token: testIdToken,
 41 | 			});
 42 | 		}),
 43 | 	];
 44 | 
 45 | 	server.listen({ onUnhandledRequest: "bypass" });
 46 | 	server.use(...handlers);
 47 | });
 48 | 
 49 | afterEach(() => {
 50 | 	server.resetHandlers();
 51 | 	server.use(...handlers);
 52 | });
 53 | 
 54 | afterAll(() => server.close());
 55 | 
 56 | describe("oauth-proxy", async () => {
 57 | 	it("should redirect to proxy url", async () => {
 58 | 		const { client, cookieSetter } = await getTestInstance({
 59 | 			plugins: [
 60 | 				oAuthProxy({
 61 | 					currentURL: "http://preview-localhost:3000",
 62 | 				}),
 63 | 			],
 64 | 			socialProviders: {
 65 | 				google: {
 66 | 					clientId: "test",
 67 | 					clientSecret: "test",
 68 | 				},
 69 | 			},
 70 | 		});
 71 | 		const headers = new Headers();
 72 | 		const res = await client.signIn.social(
 73 | 			{
 74 | 				provider: "google",
 75 | 				callbackURL: "/dashboard",
 76 | 			},
 77 | 			{
 78 | 				throw: true,
 79 | 			},
 80 | 		);
 81 | 		const state = new URL(res.url!).searchParams.get("state");
 82 | 		await client.$fetch(`/callback/google?code=test&state=${state}`, {
 83 | 			headers,
 84 | 			onError(context) {
 85 | 				const location = context.response.headers.get("location") ?? "";
 86 | 				if (!location) {
 87 | 					throw new Error("Location header not found");
 88 | 				}
 89 | 				expect(location).toContain(
 90 | 					"http://preview-localhost:3000/api/auth/oauth-proxy-callback?callbackURL=%2Fdashboard",
 91 | 				);
 92 | 				const cookies = new URL(location).searchParams.get("cookies");
 93 | 				expect(cookies).toBeTruthy();
 94 | 			},
 95 | 		});
 96 | 	});
 97 | 
 98 | 	it("shouldn't redirect to proxy url on same origin", async () => {
 99 | 		const { client, cookieSetter } = await getTestInstance({
100 | 			plugins: [oAuthProxy()],
101 | 			socialProviders: {
102 | 				google: {
103 | 					clientId: "test",
104 | 					clientSecret: "test",
105 | 				},
106 | 			},
107 | 		});
108 | 		const headers = new Headers();
109 | 		const res = await client.signIn.social(
110 | 			{
111 | 				provider: "google",
112 | 				callbackURL: "/dashboard",
113 | 			},
114 | 			{
115 | 				throw: true,
116 | 				onSuccess: cookieSetter(headers),
117 | 			},
118 | 		);
119 | 		const state = new URL(res.url!).searchParams.get("state");
120 | 		await client.$fetch(`/callback/google?code=test&state=${state}`, {
121 | 			onError(context) {
122 | 				const location = context.response.headers.get("location");
123 | 				if (!location) {
124 | 					throw new Error("Location header not found");
125 | 				}
126 | 				expect(location).not.toContain("/api/auth/oauth-proxy-callback");
127 | 				expect(location).toContain("/dashboard");
128 | 			},
129 | 		});
130 | 	});
131 | 
132 | 	it("should proxy to the original request url", async () => {
133 | 		const { client } = await getTestInstance({
134 | 			baseURL: "https://myapp.com",
135 | 			plugins: [
136 | 				oAuthProxy({
137 | 					productionURL: "https://login.myapp.com",
138 | 				}),
139 | 			],
140 | 			socialProviders: {
141 | 				google: {
142 | 					clientId: "test",
143 | 					clientSecret: "test",
144 | 				},
145 | 			},
146 | 		});
147 | 		const res = await client.signIn.social(
148 | 			{
149 | 				provider: "google",
150 | 				callbackURL: "/dashboard",
151 | 			},
152 | 			{
153 | 				throw: true,
154 | 			},
155 | 		);
156 | 		const state = new URL(res.url!).searchParams.get("state");
157 | 		await client.$fetch(`/callback/google?code=test&state=${state}`, {
158 | 			onError(context) {
159 | 				const location = context.response.headers.get("location");
160 | 				if (!location) {
161 | 					throw new Error("Location header not found");
162 | 				}
163 | 				expect(location).toContain(
164 | 					"https://myapp.com/api/auth/oauth-proxy-callback?callbackURL=%2Fdashboard",
165 | 				);
166 | 				const cookies = new URL(location).searchParams.get("cookies");
167 | 				expect(cookies).toBeTruthy();
168 | 			},
169 | 		});
170 | 	});
171 | 
172 | 	it("should require state cookie if it's not in proxy url", async () => {
173 | 		const { client } = await getTestInstance({
174 | 			baseURL: "https://myapp.com",
175 | 			plugins: [
176 | 				oAuthProxy({
177 | 					productionURL: "https://myapp.com",
178 | 				}),
179 | 			],
180 | 			socialProviders: {
181 | 				google: {
182 | 					clientId: "test",
183 | 					clientSecret: "test",
184 | 				},
185 | 			},
186 | 		});
187 | 		const res = await client.signIn.social(
188 | 			{
189 | 				provider: "google",
190 | 				callbackURL: "/dashboard",
191 | 			},
192 | 			{
193 | 				throw: true,
194 | 			},
195 | 		);
196 | 		const state = new URL(res.url!).searchParams.get("state");
197 | 		await client.$fetch(`/callback/google?code=test&state=${state}`, {
198 | 			onError(context) {
199 | 				const location = context.response.headers.get("location");
200 | 				if (!location) {
201 | 					throw new Error("Location header not found");
202 | 				}
203 | 				expect(location).toContain("state_mismatch");
204 | 			},
205 | 		});
206 | 	});
207 | 
208 | 	it("shouldn't redirect to proxy url on same origin", async () => {
209 | 		const { client, cookieSetter } = await getTestInstance({
210 | 			baseURL: "https://myapp.com",
211 | 			plugins: [
212 | 				oAuthProxy({
213 | 					productionURL: "https://myapp.com",
214 | 				}),
215 | 			],
216 | 			socialProviders: {
217 | 				google: {
218 | 					clientId: "test",
219 | 					clientSecret: "test",
220 | 				},
221 | 			},
222 | 		});
223 | 		const headers = new Headers();
224 | 		const res = await client.signIn.social(
225 | 			{
226 | 				provider: "google",
227 | 				callbackURL: "/dashboard",
228 | 			},
229 | 			{
230 | 				throw: true,
231 | 				onSuccess: cookieSetter(headers),
232 | 			},
233 | 		);
234 | 		const state = new URL(res.url!).searchParams.get("state");
235 | 		await client.$fetch(`/callback/google?code=test&state=${state}`, {
236 | 			headers,
237 | 			onError(context) {
238 | 				const location = context.response.headers.get("location");
239 | 				if (!location) {
240 | 					throw new Error("Location header not found");
241 | 				}
242 | 				expect(location).not.toContain("/api/auth/oauth-proxy-callback");
243 | 				expect(location).toContain("/dashboard");
244 | 			},
245 | 		});
246 | 	});
247 | });
248 | 
```

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

```typescript
  1 | import { betterFetch } from "@better-fetch/fetch";
  2 | import type { OAuthProvider, ProviderOptions } from "../oauth2";
  3 | import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
  4 | 
  5 | /**
  6 |  * [More info](https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info/)
  7 |  */
  8 | export interface TiktokProfile extends Record<string, any> {
  9 | 	data: {
 10 | 		user: {
 11 | 			/**
 12 | 			 * The unique identification of the user in the current application.Open id
 13 | 			 * for the client.
 14 | 			 *
 15 | 			 * To return this field, add `fields=open_id` in the user profile request's query parameter.
 16 | 			 */
 17 | 			open_id: string;
 18 | 			/**
 19 | 			 * The unique identification of the user across different apps for the same developer.
 20 | 			 * For example, if a partner has X number of clients,
 21 | 			 * it will get X number of open_id for the same TikTok user,
 22 | 			 * but one persistent union_id for the particular user.
 23 | 			 *
 24 | 			 * To return this field, add `fields=union_id` in the user profile request's query parameter.
 25 | 			 */
 26 | 			union_id?: string;
 27 | 			/**
 28 | 			 * User's profile image.
 29 | 			 *
 30 | 			 * To return this field, add `fields=avatar_url` in the user profile request's query parameter.
 31 | 			 */
 32 | 			avatar_url?: string;
 33 | 			/**
 34 | 			 * User`s profile image in 100x100 size.
 35 | 			 *
 36 | 			 * To return this field, add `fields=avatar_url_100` in the user profile request's query parameter.
 37 | 			 */
 38 | 			avatar_url_100?: string;
 39 | 			/**
 40 | 			 * User's profile image with higher resolution
 41 | 			 *
 42 | 			 * To return this field, add `fields=avatar_url_100` in the user profile request's query parameter.
 43 | 			 */
 44 | 			avatar_large_url: string;
 45 | 			/**
 46 | 			 * User's profile name
 47 | 			 *
 48 | 			 * To return this field, add `fields=display_name` in the user profile request's query parameter.
 49 | 			 */
 50 | 			display_name: string;
 51 | 			/**
 52 | 			 * User's username.
 53 | 			 *
 54 | 			 * To return this field, add `fields=username` in the user profile request's query parameter.
 55 | 			 */
 56 | 			username: string;
 57 | 			/** @note Email is currently unsupported by TikTok  */
 58 | 			email?: string;
 59 | 			/**
 60 | 			 * User's bio description if there is a valid one.
 61 | 			 *
 62 | 			 * To return this field, add `fields=bio_description` in the user profile request's query parameter.
 63 | 			 */
 64 | 			bio_description?: string;
 65 | 			/**
 66 | 			 * The link to user's TikTok profile page.
 67 | 			 *
 68 | 			 * To return this field, add `fields=profile_deep_link` in the user profile request's query parameter.
 69 | 			 */
 70 | 			profile_deep_link?: string;
 71 | 			/**
 72 | 			 * Whether TikTok has provided a verified badge to the account after confirming
 73 | 			 * that it belongs to the user it represents.
 74 | 			 *
 75 | 			 * To return this field, add `fields=is_verified` in the user profile request's query parameter.
 76 | 			 */
 77 | 			is_verified?: boolean;
 78 | 			/**
 79 | 			 * User's followers count.
 80 | 			 *
 81 | 			 * To return this field, add `fields=follower_count` in the user profile request's query parameter.
 82 | 			 */
 83 | 			follower_count?: number;
 84 | 			/**
 85 | 			 * The number of accounts that the user is following.
 86 | 			 *
 87 | 			 * To return this field, add `fields=following_count` in the user profile request's query parameter.
 88 | 			 */
 89 | 			following_count?: number;
 90 | 			/**
 91 | 			 * The total number of likes received by the user across all of their videos.
 92 | 			 *
 93 | 			 * To return this field, add `fields=likes_count` in the user profile request's query parameter.
 94 | 			 */
 95 | 			likes_count?: number;
 96 | 			/**
 97 | 			 * The total number of publicly posted videos by the user.
 98 | 			 *
 99 | 			 * To return this field, add `fields=video_count` in the user profile request's query parameter.
100 | 			 */
101 | 			video_count?: number;
102 | 		};
103 | 	};
104 | 	error?: {
105 | 		/**
106 | 		 * The error category in string.
107 | 		 */
108 | 		code?: string;
109 | 		/**
110 | 		 * The error message in string.
111 | 		 */
112 | 		message?: string;
113 | 		/**
114 | 		 * The error message in string.
115 | 		 */
116 | 		log_id?: string;
117 | 	};
118 | }
119 | 
120 | export interface TiktokOptions extends ProviderOptions {
121 | 	// Client ID is not used in TikTok, we delete it from the options
122 | 	clientId?: never;
123 | 	clientSecret: string;
124 | 	clientKey: string;
125 | }
126 | 
127 | export const tiktok = (options: TiktokOptions) => {
128 | 	return {
129 | 		id: "tiktok",
130 | 		name: "TikTok",
131 | 		createAuthorizationURL({ state, scopes, redirectURI }) {
132 | 			const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"];
133 | 			options.scope && _scopes.push(...options.scope);
134 | 			scopes && _scopes.push(...scopes);
135 | 			return new URL(
136 | 				`https://www.tiktok.com/v2/auth/authorize?scope=${_scopes.join(
137 | 					",",
138 | 				)}&response_type=code&client_key=${options.clientKey}&redirect_uri=${encodeURIComponent(
139 | 					options.redirectURI || redirectURI,
140 | 				)}&state=${state}`,
141 | 			);
142 | 		},
143 | 
144 | 		validateAuthorizationCode: async ({ code, redirectURI }) => {
145 | 			return validateAuthorizationCode({
146 | 				code,
147 | 				redirectURI: options.redirectURI || redirectURI,
148 | 				options: {
149 | 					clientKey: options.clientKey,
150 | 					clientSecret: options.clientSecret,
151 | 				},
152 | 				tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/",
153 | 			});
154 | 		},
155 | 		refreshAccessToken: options.refreshAccessToken
156 | 			? options.refreshAccessToken
157 | 			: async (refreshToken) => {
158 | 					return refreshAccessToken({
159 | 						refreshToken,
160 | 						options: {
161 | 							clientSecret: options.clientSecret,
162 | 						},
163 | 						tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/",
164 | 						authentication: "post",
165 | 						extraParams: {
166 | 							client_key: options.clientKey,
167 | 						},
168 | 					});
169 | 				},
170 | 		async getUserInfo(token) {
171 | 			if (options.getUserInfo) {
172 | 				return options.getUserInfo(token);
173 | 			}
174 | 
175 | 			const fields = [
176 | 				"open_id",
177 | 				"avatar_large_url",
178 | 				"display_name",
179 | 				"username",
180 | 			];
181 | 			const { data: profile, error } = await betterFetch<TiktokProfile>(
182 | 				`https://open.tiktokapis.com/v2/user/info/?fields=${fields.join(",")}`,
183 | 				{
184 | 					headers: {
185 | 						authorization: `Bearer ${token.accessToken}`,
186 | 					},
187 | 				},
188 | 			);
189 | 
190 | 			if (error) {
191 | 				return null;
192 | 			}
193 | 
194 | 			return {
195 | 				user: {
196 | 					email: profile.data.user.email || profile.data.user.username,
197 | 					id: profile.data.user.open_id,
198 | 					name: profile.data.user.display_name || profile.data.user.username,
199 | 					image: profile.data.user.avatar_large_url,
200 | 					/** @note Tiktok does not provide emailVerified or even email*/
201 | 					emailVerified: profile.data.user.email ? true : false,
202 | 				},
203 | 				data: profile,
204 | 			};
205 | 		},
206 | 		options,
207 | 	} satisfies OAuthProvider<TiktokProfile, TiktokOptions>;
208 | };
209 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/oauth2/link-account.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { APIError, createEmailVerificationToken } from "../api";
  2 | import type { Account } from "../types";
  3 | import type { User } from "../types";
  4 | import { logger } from "@better-auth/core/env";
  5 | import { isDevelopment } from "@better-auth/core/env";
  6 | import { setTokenUtil } from "./utils";
  7 | import type { GenericEndpointContext } from "@better-auth/core";
  8 | 
  9 | export async function handleOAuthUserInfo(
 10 | 	c: GenericEndpointContext,
 11 | 	{
 12 | 		userInfo,
 13 | 		account,
 14 | 		callbackURL,
 15 | 		disableSignUp,
 16 | 		overrideUserInfo,
 17 | 	}: {
 18 | 		userInfo: Omit<User, "createdAt" | "updatedAt">;
 19 | 		account: Omit<Account, "id" | "userId" | "createdAt" | "updatedAt">;
 20 | 		callbackURL?: string;
 21 | 		disableSignUp?: boolean;
 22 | 		overrideUserInfo?: boolean;
 23 | 	},
 24 | ) {
 25 | 	const dbUser = await c.context.internalAdapter
 26 | 		.findOAuthUser(
 27 | 			userInfo.email.toLowerCase(),
 28 | 			account.accountId,
 29 | 			account.providerId,
 30 | 		)
 31 | 		.catch((e) => {
 32 | 			logger.error(
 33 | 				"Better auth was unable to query your database.\nError: ",
 34 | 				e,
 35 | 			);
 36 | 			const errorURL =
 37 | 				c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
 38 | 			throw c.redirect(`${errorURL}?error=internal_server_error`);
 39 | 		});
 40 | 	let user = dbUser?.user;
 41 | 	let isRegister = !user;
 42 | 
 43 | 	if (dbUser) {
 44 | 		const hasBeenLinked = dbUser.accounts.find(
 45 | 			(a) =>
 46 | 				a.providerId === account.providerId &&
 47 | 				a.accountId === account.accountId,
 48 | 		);
 49 | 		if (!hasBeenLinked) {
 50 | 			const trustedProviders =
 51 | 				c.context.options.account?.accountLinking?.trustedProviders;
 52 | 			const isTrustedProvider = trustedProviders?.includes(
 53 | 				account.providerId as "apple",
 54 | 			);
 55 | 			if (
 56 | 				(!isTrustedProvider && !userInfo.emailVerified) ||
 57 | 				c.context.options.account?.accountLinking?.enabled === false
 58 | 			) {
 59 | 				if (isDevelopment()) {
 60 | 					logger.warn(
 61 | 						`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`,
 62 | 					);
 63 | 				}
 64 | 				return {
 65 | 					error: "account not linked",
 66 | 					data: null,
 67 | 				};
 68 | 			}
 69 | 			try {
 70 | 				await c.context.internalAdapter.linkAccount({
 71 | 					providerId: account.providerId,
 72 | 					accountId: userInfo.id.toString(),
 73 | 					userId: dbUser.user.id,
 74 | 					accessToken: await setTokenUtil(account.accessToken, c.context),
 75 | 					refreshToken: await setTokenUtil(account.refreshToken, c.context),
 76 | 					idToken: account.idToken,
 77 | 					accessTokenExpiresAt: account.accessTokenExpiresAt,
 78 | 					refreshTokenExpiresAt: account.refreshTokenExpiresAt,
 79 | 					scope: account.scope,
 80 | 				});
 81 | 			} catch (e) {
 82 | 				logger.error("Unable to link account", e);
 83 | 				return {
 84 | 					error: "unable to link account",
 85 | 					data: null,
 86 | 				};
 87 | 			}
 88 | 
 89 | 			if (
 90 | 				userInfo.emailVerified &&
 91 | 				!dbUser.user.emailVerified &&
 92 | 				userInfo.email.toLowerCase() === dbUser.user.email
 93 | 			) {
 94 | 				await c.context.internalAdapter.updateUser(dbUser.user.id, {
 95 | 					emailVerified: true,
 96 | 				});
 97 | 			}
 98 | 		} else {
 99 | 			if (c.context.options.account?.updateAccountOnSignIn !== false) {
100 | 				const updateData = Object.fromEntries(
101 | 					Object.entries({
102 | 						idToken: account.idToken,
103 | 						accessToken: await setTokenUtil(account.accessToken, c.context),
104 | 						refreshToken: await setTokenUtil(account.refreshToken, c.context),
105 | 						accessTokenExpiresAt: account.accessTokenExpiresAt,
106 | 						refreshTokenExpiresAt: account.refreshTokenExpiresAt,
107 | 						scope: account.scope,
108 | 					}).filter(([_, value]) => value !== undefined),
109 | 				);
110 | 
111 | 				if (Object.keys(updateData).length > 0) {
112 | 					await c.context.internalAdapter.updateAccount(
113 | 						hasBeenLinked.id,
114 | 						updateData,
115 | 					);
116 | 				}
117 | 			}
118 | 
119 | 			if (
120 | 				userInfo.emailVerified &&
121 | 				!dbUser.user.emailVerified &&
122 | 				userInfo.email.toLowerCase() === dbUser.user.email
123 | 			) {
124 | 				await c.context.internalAdapter.updateUser(dbUser.user.id, {
125 | 					emailVerified: true,
126 | 				});
127 | 			}
128 | 		}
129 | 		if (overrideUserInfo) {
130 | 			const { id: _, ...restUserInfo } = userInfo;
131 | 			// update user info from the provider if overrideUserInfo is true
132 | 			await c.context.internalAdapter.updateUser(dbUser.user.id, {
133 | 				...restUserInfo,
134 | 				email: userInfo.email.toLowerCase(),
135 | 				emailVerified:
136 | 					userInfo.email.toLowerCase() === dbUser.user.email
137 | 						? dbUser.user.emailVerified || userInfo.emailVerified
138 | 						: userInfo.emailVerified,
139 | 			});
140 | 		}
141 | 	} else {
142 | 		if (disableSignUp) {
143 | 			return {
144 | 				error: "signup disabled",
145 | 				data: null,
146 | 				isRegister: false,
147 | 			};
148 | 		}
149 | 		try {
150 | 			const { id: _, ...restUserInfo } = userInfo;
151 | 			user = await c.context.internalAdapter
152 | 				.createOAuthUser(
153 | 					{
154 | 						...restUserInfo,
155 | 						email: userInfo.email.toLowerCase(),
156 | 					},
157 | 					{
158 | 						accessToken: await setTokenUtil(account.accessToken, c.context),
159 | 						refreshToken: await setTokenUtil(account.refreshToken, c.context),
160 | 						idToken: account.idToken,
161 | 						accessTokenExpiresAt: account.accessTokenExpiresAt,
162 | 						refreshTokenExpiresAt: account.refreshTokenExpiresAt,
163 | 						scope: account.scope,
164 | 						providerId: account.providerId,
165 | 						accountId: userInfo.id.toString(),
166 | 					},
167 | 				)
168 | 				.then((res) => res?.user);
169 | 			if (
170 | 				!userInfo.emailVerified &&
171 | 				user &&
172 | 				c.context.options.emailVerification?.sendOnSignUp
173 | 			) {
174 | 				const token = await createEmailVerificationToken(
175 | 					c.context.secret,
176 | 					user.email,
177 | 					undefined,
178 | 					c.context.options.emailVerification?.expiresIn,
179 | 				);
180 | 				const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
181 | 				await c.context.options.emailVerification?.sendVerificationEmail?.(
182 | 					{
183 | 						user,
184 | 						url,
185 | 						token,
186 | 					},
187 | 					c.request,
188 | 				);
189 | 			}
190 | 		} catch (e: any) {
191 | 			logger.error(e);
192 | 			if (e instanceof APIError) {
193 | 				return {
194 | 					error: e.message,
195 | 					data: null,
196 | 					isRegister: false,
197 | 				};
198 | 			}
199 | 			return {
200 | 				error: "unable to create user",
201 | 				data: null,
202 | 				isRegister: false,
203 | 			};
204 | 		}
205 | 	}
206 | 	if (!user) {
207 | 		return {
208 | 			error: "unable to create user",
209 | 			data: null,
210 | 			isRegister: false,
211 | 		};
212 | 	}
213 | 
214 | 	const session = await c.context.internalAdapter.createSession(user.id);
215 | 	if (!session) {
216 | 		return {
217 | 			error: "unable to create session",
218 | 			data: null,
219 | 			isRegister: false,
220 | 		};
221 | 	}
222 | 	return {
223 | 		data: {
224 | 			session,
225 | 			user,
226 | 		},
227 | 		error: null,
228 | 		isRegister,
229 | 	};
230 | }
231 | 
```

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

```markdown
  1 | ---
  2 | title: Salesforce
  3 | description: Salesforce provider setup and usage.
  4 | ---
  5 | 
  6 | <Steps>
  7 |     <Step>
  8 |         ### Get your Salesforce Credentials
  9 |        1. Log into your Salesforce org (Production or Developer Edition)
 10 |     2. Navigate to **Setup > App Manager**
 11 |     3. Click **New Connected App**
 12 |     4. Fill in the basic information:
 13 |        - Connected App Name: Your app name
 14 |        - API Name: Auto-generated from app name
 15 |        - Contact Email: Your email address
 16 |     5. Enable OAuth Settings:
 17 |        - Check **Enable OAuth Settings**
 18 |        - Set **Callback URL** to your redirect URI (e.g., `http://localhost:3000/api/auth/callback/salesforce` for development)
 19 |        - Select Required OAuth Scopes:
 20 |          - Access your basic information (id)
 21 |          - Access your identity URL service (openid)
 22 |          - Access your email address (email)
 23 |          - Perform requests on your behalf at any time (refresh_token, offline_access)
 24 |     6. Enable **Require Proof Key for Code Exchange (PKCE)** (required)
 25 |     7. Save and note your **Consumer Key** (Client ID) and **Consumer Secret** (Client Secret)
 26 | 
 27 |     <Callout type="info">
 28 |         - For development, you can use `http://localhost:3000` URLs, but production requires HTTPS
 29 |         - The callback URL must exactly match what's configured in Better Auth
 30 |         - PKCE (Proof Key for Code Exchange) is required by Salesforce and is automatically handled by the provider
 31 |     </Callout>
 32 | 
 33 |     <Callout type="warning">
 34 |         For sandbox testing, you can create the Connected App in your sandbox org, or use the same Connected App but specify `environment: "sandbox"` in the provider configuration.
 35 |     </Callout>
 36 | 
 37 |     </Step>
 38 | 
 39 |   <Step>
 40 |   ### Configure the provider
 41 |     To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
 42 | 
 43 |     ```ts title="auth.ts"
 44 |     import { betterAuth } from "better-auth"
 45 | 
 46 |     export const auth = betterAuth({
 47 |         socialProviders: {
 48 |             salesforce: { // [!code highlight]
 49 |                 clientId: process.env.SALESFORCE_CLIENT_ID as string, // [!code highlight]
 50 |                 clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string, // [!code highlight]
 51 |                 environment: "production", // or "sandbox" // [!code highlight]
 52 |             }, // [!code highlight]
 53 |         },
 54 |     })
 55 |     ```
 56 | 
 57 |     #### Configuration Options
 58 | 
 59 |     - `clientId`: Your Connected App's Consumer Key
 60 |     - `clientSecret`: Your Connected App's Consumer Secret  
 61 |     - `environment`: `"production"` (default) or `"sandbox"`
 62 |     - `loginUrl`: Custom My Domain URL (without `https://`) - overrides environment setting
 63 |     - `redirectURI`: Override the auto-generated redirect URI if needed
 64 | 
 65 |     #### Advanced Configuration
 66 | 
 67 |     ```ts title="auth.ts"
 68 |     export const auth = betterAuth({
 69 |         socialProviders: {
 70 |             salesforce: {
 71 |                 clientId: process.env.SALESFORCE_CLIENT_ID as string,
 72 |                 clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string,
 73 |                 environment: "sandbox", // [!code highlight]
 74 |                 loginUrl: "mycompany.my.salesforce.com", // Custom My Domain // [!code highlight]
 75 |                 redirectURI: "http://localhost:3000/api/auth/callback/salesforce", // Override if needed // [!code highlight]
 76 |             },
 77 |         },
 78 |     })
 79 |     ```
 80 | 
 81 |     <Callout type="info">
 82 |         - Use `environment: "sandbox"` for testing with Salesforce sandbox orgs
 83 |         - The `loginUrl` option is useful for organizations with My Domain enabled
 84 |         - The `redirectURI` option helps resolve redirect URI mismatch errors
 85 |     </Callout>
 86 |     </Step>
 87 |         
 88 |    <Step>
 89 |     ### Environment Variables
 90 |     Add the following environment variables to your `.env.local` file:
 91 | 
 92 |     ```bash title=".env.local"
 93 |     SALESFORCE_CLIENT_ID=your_consumer_key_here
 94 |     SALESFORCE_CLIENT_SECRET=your_consumer_secret_here
 95 |     BETTER_AUTH_URL=http://localhost:3000 # Important for redirect URI generation
 96 |     ```
 97 | 
 98 |     For production:
 99 |     ```bash title=".env"
100 |     SALESFORCE_CLIENT_ID=your_consumer_key_here
101 |     SALESFORCE_CLIENT_SECRET=your_consumer_secret_here
102 |     BETTER_AUTH_URL=https://yourdomain.com
103 |     ```
104 |     </Step>
105 | 
106 |     <Step>
107 |     ### Sign In with Salesforce
108 |     To sign in with Salesforce, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
109 |     - `provider`: The provider to use. It should be set to `salesforce`.
110 | 
111 |     ```ts title="auth-client.ts"
112 |     import { createAuthClient } from "better-auth/client"
113 |     const authClient = createAuthClient()
114 | 
115 |     const signIn = async () => {
116 |         const data = await authClient.signIn.social({
117 |             provider: "salesforce"
118 |         })
119 |     }
120 |     ```
121 | </Step>
122 | <Step>
123 |     ### Troubleshooting
124 | 
125 |     #### Redirect URI Mismatch Error
126 |     If you encounter a `redirect_uri_mismatch` error:
127 | 
128 |     1. **Check Callback URL**: Ensure the Callback URL in your Salesforce Connected App exactly matches your Better Auth callback URL
129 |     2. **Protocol**: Make sure you're using the same protocol (`http://` vs `https://`)
130 |     3. **Port**: Verify the port number matches (e.g., `:3000`)
131 |     4. **Override if needed**: Use the `redirectURI` option to explicitly set the redirect URI
132 | 
133 |     ```ts
134 |     salesforce: {
135 |         clientId: process.env.SALESFORCE_CLIENT_ID as string,
136 |         clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string,
137 |         redirectURI: "http://localhost:3000/api/auth/callback/salesforce", // [!code highlight]
138 |     }
139 |     ```
140 | 
141 |     #### Environment Issues
142 |     - **Production**: Use `environment: "production"` (default) with `login.salesforce.com`
143 |     - **Sandbox**: Use `environment: "sandbox"` with `test.salesforce.com`
144 |     - **My Domain**: Use `loginUrl: "yourcompany.my.salesforce.com"` for custom domains
145 | 
146 |     #### PKCE Requirements
147 |     Salesforce requires PKCE (Proof Key for Code Exchange) which is automatically handled by this provider. Make sure PKCE is enabled in your Connected App settings.
148 | 
149 |     <Callout type="info">
150 |         The default scopes requested are `openid`, `email`, and `profile`. The provider will automatically include the `id` scope for accessing basic user information.
151 |     </Callout>
152 | </Step>
153 | </Steps>
154 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll } from "vitest";
  2 | import { Kysely, sql } from "kysely";
  3 | import { NodeSqliteDialect } from "../node-sqlite-dialect";
  4 | import { kyselyAdapter } from "../kysely-adapter";
  5 | import { runAdapterTest } from "../../test";
  6 | import { getMigrations } from "../../../db/get-migration";
  7 | import type { BetterAuthOptions } from "@better-auth/core";
  8 | import merge from "deepmerge";
  9 | import type { DatabaseSync } from "node:sqlite";
 10 | const nodeVersion = process.version;
 11 | const nodeSqliteSupported = +nodeVersion.split(".")[0]!.slice(1) >= 22;
 12 | 
 13 | describe.runIf(nodeSqliteSupported)("node-sqlite-dialect", async () => {
 14 | 	let db: DatabaseSync;
 15 | 	let kysely: Kysely<any>;
 16 | 
 17 | 	beforeAll(async () => {
 18 | 		if (!nodeSqliteSupported) {
 19 | 			return;
 20 | 		}
 21 | 		const { DatabaseSync } = await import("node:sqlite");
 22 | 
 23 | 		db = new DatabaseSync(":memory:");
 24 | 
 25 | 		kysely = new Kysely({
 26 | 			dialect: new NodeSqliteDialect({
 27 | 				database: db,
 28 | 			}),
 29 | 		});
 30 | 	});
 31 | 
 32 | 	afterAll(async () => {
 33 | 		if (!nodeSqliteSupported) {
 34 | 			return;
 35 | 		}
 36 | 		await kysely.destroy();
 37 | 	});
 38 | 
 39 | 	describe("basic operations", () => {
 40 | 		it("should create tables", async () => {
 41 | 			await kysely.schema
 42 | 				.createTable("test_table")
 43 | 				.addColumn("id", "integer", (col) => col.primaryKey())
 44 | 				.addColumn("name", "text", (col) => col.notNull())
 45 | 				.addColumn("created_at", "timestamp", (col) =>
 46 | 					col.defaultTo(sql`CURRENT_TIMESTAMP`),
 47 | 				)
 48 | 				.execute();
 49 | 
 50 | 			const tables = await kysely.introspection.getTables();
 51 | 			const testTable = tables.find((t) => t.name === "test_table");
 52 | 			expect(testTable).toBeDefined();
 53 | 			expect(testTable?.columns).toHaveLength(3);
 54 | 		});
 55 | 
 56 | 		it("should insert and select data", async () => {
 57 | 			await kysely
 58 | 				.insertInto("test_table")
 59 | 				.values({ id: 1, name: "Test User" })
 60 | 				.execute();
 61 | 
 62 | 			const result = await kysely
 63 | 				.selectFrom("test_table")
 64 | 				.selectAll()
 65 | 				.execute();
 66 | 
 67 | 			expect(result).toHaveLength(1);
 68 | 			expect(result[0]).toMatchObject({
 69 | 				id: 1,
 70 | 				name: "Test User",
 71 | 			});
 72 | 		});
 73 | 
 74 | 		it("should update data", async () => {
 75 | 			await kysely
 76 | 				.updateTable("test_table")
 77 | 				.set({ name: "Updated User" })
 78 | 				.where("id", "=", 1)
 79 | 				.execute();
 80 | 
 81 | 			const result = await kysely
 82 | 				.selectFrom("test_table")
 83 | 				.where("id", "=", 1)
 84 | 				.selectAll()
 85 | 				.executeTakeFirst();
 86 | 
 87 | 			expect(result?.name).toBe("Updated User");
 88 | 		});
 89 | 
 90 | 		it("should delete data", async () => {
 91 | 			await kysely.deleteFrom("test_table").where("id", "=", 1).execute();
 92 | 
 93 | 			const result = await kysely
 94 | 				.selectFrom("test_table")
 95 | 				.selectAll()
 96 | 				.execute();
 97 | 
 98 | 			expect(result).toHaveLength(0);
 99 | 		});
100 | 
101 | 		it("should handle transactions", async () => {
102 | 			await kysely.transaction().execute(async (trx) => {
103 | 				await trx
104 | 					.insertInto("test_table")
105 | 					.values({ id: 2, name: "Transaction Test" })
106 | 					.execute();
107 | 
108 | 				const result = await trx
109 | 					.selectFrom("test_table")
110 | 					.where("id", "=", 2)
111 | 					.selectAll()
112 | 					.executeTakeFirst();
113 | 
114 | 				expect(result?.name).toBe("Transaction Test");
115 | 			});
116 | 
117 | 			// Verify the transaction was committed
118 | 			const result = await kysely
119 | 				.selectFrom("test_table")
120 | 				.where("id", "=", 2)
121 | 				.selectAll()
122 | 				.executeTakeFirst();
123 | 
124 | 			expect(result?.name).toBe("Transaction Test");
125 | 		});
126 | 
127 | 		it("should rollback transactions on error", async () => {
128 | 			try {
129 | 				await kysely.transaction().execute(async (trx) => {
130 | 					await trx
131 | 						.insertInto("test_table")
132 | 						.values({ id: 3, name: "Rollback Test" })
133 | 						.execute();
134 | 
135 | 					// Force an error
136 | 					throw new Error("Test error");
137 | 				});
138 | 			} catch (error) {
139 | 				// Expected error
140 | 			}
141 | 
142 | 			// Verify the transaction was rolled back
143 | 			const result = await kysely
144 | 				.selectFrom("test_table")
145 | 				.where("id", "=", 3)
146 | 				.selectAll()
147 | 				.executeTakeFirst();
148 | 
149 | 			expect(result).toBeUndefined();
150 | 		});
151 | 	});
152 | 
153 | 	describe("introspection", () => {
154 | 		beforeAll(async () => {
155 | 			// Create a table with various column types
156 | 			await kysely.schema
157 | 				.createTable("introspection_test")
158 | 				.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
159 | 				.addColumn("name", "text", (col) => col.notNull())
160 | 				.addColumn("email", "text", (col) => col.unique())
161 | 				.addColumn("age", "integer")
162 | 				.addColumn("is_active", "boolean", (col) => col.defaultTo(true))
163 | 				.execute();
164 | 		});
165 | 
166 | 		it("should get table metadata", async () => {
167 | 			const tables = await kysely.introspection.getTables();
168 | 			const table = tables.find((t) => t.name === "introspection_test");
169 | 
170 | 			expect(table).toBeDefined();
171 | 			expect(table?.columns).toHaveLength(5);
172 | 
173 | 			const idColumn = table?.columns.find((c) => c.name === "id");
174 | 			expect(idColumn?.isAutoIncrementing).toBe(true);
175 | 			expect(idColumn?.isNullable).toBe(true); // SQLite primary keys can be NULL until a value is inserted
176 | 
177 | 			const nameColumn = table?.columns.find((c) => c.name === "name");
178 | 			expect(nameColumn?.isNullable).toBe(false);
179 | 
180 | 			const ageColumn = table?.columns.find((c) => c.name === "age");
181 | 			expect(ageColumn?.isNullable).toBe(true);
182 | 
183 | 			const isActiveColumn = table?.columns.find((c) => c.name === "is_active");
184 | 			expect(isActiveColumn?.hasDefaultValue).toBe(true);
185 | 		});
186 | 	});
187 | 
188 | 	describe("better-auth adapter integration", async () => {
189 | 		if (!nodeSqliteSupported) {
190 | 			return;
191 | 		}
192 | 		const { DatabaseSync } = await import("node:sqlite");
193 | 		const db = new DatabaseSync(":memory:");
194 | 		const betterAuthKysely = new Kysely({
195 | 			dialect: new NodeSqliteDialect({
196 | 				database: db,
197 | 			}),
198 | 		});
199 | 
200 | 		const opts: BetterAuthOptions = {
201 | 			database: {
202 | 				db: betterAuthKysely,
203 | 				type: "sqlite",
204 | 			},
205 | 			user: {
206 | 				fields: {
207 | 					email: "email_address",
208 | 				},
209 | 				additionalFields: {
210 | 					test: {
211 | 						type: "string",
212 | 						defaultValue: "test",
213 | 					},
214 | 				},
215 | 			},
216 | 			session: {
217 | 				modelName: "sessions",
218 | 			},
219 | 		};
220 | 
221 | 		beforeAll(async () => {
222 | 			const { runMigrations } = await getMigrations(opts);
223 | 			await runMigrations();
224 | 		});
225 | 
226 | 		afterAll(async () => {
227 | 			await betterAuthKysely.destroy();
228 | 		});
229 | 
230 | 		const adapter = kyselyAdapter(betterAuthKysely, {
231 | 			type: "sqlite",
232 | 			debugLogs: {
233 | 				isRunningAdapterTests: true,
234 | 			},
235 | 		});
236 | 
237 | 		runAdapterTest({
238 | 			getAdapter: async (customOptions = {}) => {
239 | 				return adapter(merge(customOptions, opts));
240 | 			},
241 | 			testPrefix: "node-sqlite",
242 | 		});
243 | 	});
244 | });
245 | 
```
Page 19/69FirstPrevNextLast