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

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── e2e.yml
│       ├── preview.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│   └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│   ├── expo-example
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app.config.ts
│   │   ├── assets
│   │   │   ├── bg-image.jpeg
│   │   │   ├── fonts
│   │   │   │   └── SpaceMono-Regular.ttf
│   │   │   ├── icon.png
│   │   │   └── images
│   │   │       ├── adaptive-icon.png
│   │   │       ├── favicon.png
│   │   │       ├── logo.png
│   │   │       ├── partial-react-logo.png
│   │   │       ├── react-logo.png
│   │   │       ├── [email protected]
│   │   │       ├── [email protected]
│   │   │       └── splash.png
│   │   ├── babel.config.js
│   │   ├── components.json
│   │   ├── expo-env.d.ts
│   │   ├── index.ts
│   │   ├── metro.config.js
│   │   ├── nativewind-env.d.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── api
│   │   │   │   │   └── auth
│   │   │   │   │       └── [...route]+api.ts
│   │   │   │   ├── dashboard.tsx
│   │   │   │   ├── forget-password.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── sign-up.tsx
│   │   │   ├── components
│   │   │   │   ├── icons
│   │   │   │   │   └── google.tsx
│   │   │   │   └── ui
│   │   │   │       ├── avatar.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       └── text.tsx
│   │   │   ├── global.css
│   │   │   └── lib
│   │   │       ├── auth-client.ts
│   │   │       ├── auth.ts
│   │   │       ├── icons
│   │   │       │   ├── iconWithClassName.ts
│   │   │       │   └── X.tsx
│   │   │       └── utils.ts
│   │   ├── tailwind.config.js
│   │   └── tsconfig.json
│   └── nextjs
│       ├── .env.example
│       ├── .gitignore
│       ├── app
│       │   ├── (auth)
│       │   │   ├── forget-password
│       │   │   │   └── page.tsx
│       │   │   ├── reset-password
│       │   │   │   └── page.tsx
│       │   │   ├── sign-in
│       │   │   │   ├── loading.tsx
│       │   │   │   └── page.tsx
│       │   │   └── two-factor
│       │   │       ├── otp
│       │   │       │   └── page.tsx
│       │   │       └── page.tsx
│       │   ├── accept-invitation
│       │   │   └── [id]
│       │   │       ├── invitation-error.tsx
│       │   │       └── page.tsx
│       │   ├── admin
│       │   │   └── page.tsx
│       │   ├── api
│       │   │   └── auth
│       │   │       └── [...all]
│       │   │           └── route.ts
│       │   ├── apps
│       │   │   └── register
│       │   │       └── page.tsx
│       │   ├── client-test
│       │   │   └── page.tsx
│       │   ├── dashboard
│       │   │   ├── change-plan.tsx
│       │   │   ├── client.tsx
│       │   │   ├── organization-card.tsx
│       │   │   ├── page.tsx
│       │   │   ├── upgrade-button.tsx
│       │   │   └── user-card.tsx
│       │   ├── device
│       │   │   ├── approve
│       │   │   │   └── page.tsx
│       │   │   ├── denied
│       │   │   │   └── page.tsx
│       │   │   ├── layout.tsx
│       │   │   ├── page.tsx
│       │   │   └── success
│       │   │       └── page.tsx
│       │   ├── favicon.ico
│       │   ├── features.tsx
│       │   ├── fonts
│       │   │   ├── GeistMonoVF.woff
│       │   │   └── GeistVF.woff
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── oauth
│       │   │   └── authorize
│       │   │       ├── concet-buttons.tsx
│       │   │       └── page.tsx
│       │   ├── page.tsx
│       │   └── pricing
│       │       └── page.tsx
│       ├── components
│       │   ├── account-switch.tsx
│       │   ├── blocks
│       │   │   └── pricing.tsx
│       │   ├── logo.tsx
│       │   ├── one-tap.tsx
│       │   ├── sign-in-btn.tsx
│       │   ├── sign-in.tsx
│       │   ├── sign-up.tsx
│       │   ├── theme-provider.tsx
│       │   ├── theme-toggle.tsx
│       │   ├── tier-labels.tsx
│       │   ├── ui
│       │   │   ├── accordion.tsx
│       │   │   ├── alert-dialog.tsx
│       │   │   ├── alert.tsx
│       │   │   ├── aspect-ratio.tsx
│       │   │   ├── avatar.tsx
│       │   │   ├── badge.tsx
│       │   │   ├── breadcrumb.tsx
│       │   │   ├── button.tsx
│       │   │   ├── calendar.tsx
│       │   │   ├── card.tsx
│       │   │   ├── carousel.tsx
│       │   │   ├── chart.tsx
│       │   │   ├── checkbox.tsx
│       │   │   ├── collapsible.tsx
│       │   │   ├── command.tsx
│       │   │   ├── context-menu.tsx
│       │   │   ├── copy-button.tsx
│       │   │   ├── dialog.tsx
│       │   │   ├── drawer.tsx
│       │   │   ├── dropdown-menu.tsx
│       │   │   ├── form.tsx
│       │   │   ├── hover-card.tsx
│       │   │   ├── input-otp.tsx
│       │   │   ├── input.tsx
│       │   │   ├── label.tsx
│       │   │   ├── menubar.tsx
│       │   │   ├── navigation-menu.tsx
│       │   │   ├── pagination.tsx
│       │   │   ├── password-input.tsx
│       │   │   ├── popover.tsx
│       │   │   ├── progress.tsx
│       │   │   ├── radio-group.tsx
│       │   │   ├── resizable.tsx
│       │   │   ├── scroll-area.tsx
│       │   │   ├── select.tsx
│       │   │   ├── separator.tsx
│       │   │   ├── sheet.tsx
│       │   │   ├── skeleton.tsx
│       │   │   ├── slider.tsx
│       │   │   ├── sonner.tsx
│       │   │   ├── switch.tsx
│       │   │   ├── table.tsx
│       │   │   ├── tabs.tsx
│       │   │   ├── tabs2.tsx
│       │   │   ├── textarea.tsx
│       │   │   ├── toast.tsx
│       │   │   ├── toaster.tsx
│       │   │   ├── toggle-group.tsx
│       │   │   ├── toggle.tsx
│       │   │   └── tooltip.tsx
│       │   └── wrapper.tsx
│       ├── components.json
│       ├── hooks
│       │   └── use-toast.ts
│       ├── lib
│       │   ├── auth-client.ts
│       │   ├── auth-types.ts
│       │   ├── auth.ts
│       │   ├── email
│       │   │   ├── invitation.tsx
│       │   │   ├── resend.ts
│       │   │   └── reset-password.tsx
│       │   ├── metadata.ts
│       │   ├── shared.ts
│       │   └── utils.ts
│       ├── middleware.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── 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-exact-optional-property-types
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── user-additional-fields.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
│   │   │   │   ├── sso
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── sso.test.ts
│   │   │   │   ├── two-factor
│   │   │   │   │   ├── backup-codes
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── constant.ts
│   │   │   │   │   ├── error-code.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── otp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── totp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── two-factor.test.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-two-factor.ts
│   │   │   │   └── username
│   │   │   │       ├── client.ts
│   │   │   │       ├── error-codes.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       └── username.test.ts
│   │   │   ├── social-providers
│   │   │   │   └── index.ts
│   │   │   ├── social.test.ts
│   │   │   ├── test-utils
│   │   │   │   ├── headers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── test-instance.ts
│   │   │   ├── types
│   │   │   │   ├── adapter.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── plugins.ts
│   │   │   │   └── types.test.ts
│   │   │   └── utils
│   │   │       ├── await-object.ts
│   │   │       ├── boolean.ts
│   │   │       ├── clone.ts
│   │   │       ├── constants.ts
│   │   │       ├── date.ts
│   │   │       ├── ensure-utc.ts
│   │   │       ├── get-request-ip.ts
│   │   │       ├── hashing.ts
│   │   │       ├── hide-metadata.ts
│   │   │       ├── id.ts
│   │   │       ├── import-util.ts
│   │   │       ├── index.ts
│   │   │       ├── is-atom.ts
│   │   │       ├── is-promise.ts
│   │   │       ├── json.ts
│   │   │       ├── merger.ts
│   │   │       ├── middleware-response.ts
│   │   │       ├── misc.ts
│   │   │       ├── password.ts
│   │   │       ├── plugin-helper.ts
│   │   │       ├── shim.ts
│   │   │       ├── time.ts
│   │   │       ├── url.ts
│   │   │       └── wildcard.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── 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
│   │   │   ├── middleware
│   │   │   │   └── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/email-otp/email-otp.test.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { describe, it, expect, vi } from "vitest";
   2 | import { getTestInstance } from "../../test-utils/test-instance";
   3 | import { emailOTP } from ".";
   4 | import { emailOTPClient } from "./client";
   5 | import { bearer } from "../bearer";
   6 | import { splitAtLastColon } from "./utils";
   7 | import { createAuthClient } from "../../client";
   8 | 
   9 | describe("email-otp", async () => {
  10 | 	const otpFn = vi.fn();
  11 | 	let otp = "";
  12 | 	const { client, testUser, auth } = await getTestInstance(
  13 | 		{
  14 | 			plugins: [
  15 | 				bearer(),
  16 | 				emailOTP({
  17 | 					async sendVerificationOTP({ email, otp: _otp, type }) {
  18 | 						otp = _otp;
  19 | 						otpFn(email, _otp, type);
  20 | 					},
  21 | 					sendVerificationOnSignUp: true,
  22 | 				}),
  23 | 			],
  24 | 			emailVerification: {
  25 | 				autoSignInAfterVerification: true,
  26 | 			},
  27 | 		},
  28 | 		{
  29 | 			clientOptions: {
  30 | 				plugins: [emailOTPClient()],
  31 | 			},
  32 | 		},
  33 | 	);
  34 | 
  35 | 	it("should verify email with otp", async () => {
  36 | 		const res = await client.emailOtp.sendVerificationOtp({
  37 | 			email: testUser.email,
  38 | 			type: "email-verification",
  39 | 		});
  40 | 		expect(res.data?.success).toBe(true);
  41 | 		expect(otp.length).toBe(6);
  42 | 		expect(otpFn).toHaveBeenCalledWith(
  43 | 			testUser.email,
  44 | 			otp,
  45 | 			"email-verification",
  46 | 		);
  47 | 		const verifiedUser = await client.emailOtp.verifyEmail({
  48 | 			email: testUser.email,
  49 | 			otp,
  50 | 		});
  51 | 		expect(verifiedUser.data?.status).toBe(true);
  52 | 	});
  53 | 
  54 | 	it("should sign-in with otp", async () => {
  55 | 		const res = await client.emailOtp.sendVerificationOtp({
  56 | 			email: testUser.email,
  57 | 			type: "sign-in",
  58 | 		});
  59 | 		expect(res.data?.success).toBe(true);
  60 | 		expect(otp.length).toBe(6);
  61 | 		expect(otpFn).toHaveBeenCalledWith(testUser.email, otp, "sign-in");
  62 | 		const verifiedUser = await client.signIn.emailOtp(
  63 | 			{
  64 | 				email: testUser.email,
  65 | 				otp,
  66 | 			},
  67 | 			{
  68 | 				onSuccess: (ctx) => {
  69 | 					const header = ctx.response.headers.get("set-cookie");
  70 | 					expect(header).toContain("better-auth.session_token");
  71 | 				},
  72 | 			},
  73 | 		);
  74 | 		expect(verifiedUser.data?.token).toBeDefined();
  75 | 	});
  76 | 
  77 | 	it("should sign-up with otp", async () => {
  78 | 		const testUser2 = {
  79 | 			email: "[email protected]",
  80 | 		};
  81 | 		await client.emailOtp.sendVerificationOtp({
  82 | 			email: testUser2.email,
  83 | 			type: "sign-in",
  84 | 		});
  85 | 		const newUser = await client.signIn.emailOtp(
  86 | 			{
  87 | 				email: testUser2.email,
  88 | 				otp,
  89 | 			},
  90 | 			{
  91 | 				onSuccess: (ctx) => {
  92 | 					const header = ctx.response.headers.get("set-cookie");
  93 | 					expect(header).toContain("better-auth.session_token");
  94 | 				},
  95 | 			},
  96 | 		);
  97 | 		expect(newUser.data?.token).toBeDefined();
  98 | 	});
  99 | 
 100 | 	it("should send verification otp on sign-up", async () => {
 101 | 		const testUser2 = {
 102 | 			email: "[email protected]",
 103 | 			password: "password",
 104 | 			name: "test",
 105 | 		};
 106 | 		await client.signUp.email(testUser2);
 107 | 		expect(otpFn).toHaveBeenCalledWith(
 108 | 			testUser2.email,
 109 | 			otp,
 110 | 			"email-verification",
 111 | 		);
 112 | 	});
 113 | 
 114 | 	it("should send forget password otp", async () => {
 115 | 		await client.emailOtp.sendVerificationOtp({
 116 | 			email: testUser.email,
 117 | 			type: "forget-password",
 118 | 		});
 119 | 	});
 120 | 
 121 | 	it("should reset password", async () => {
 122 | 		await client.emailOtp.resetPassword({
 123 | 			email: testUser.email,
 124 | 			otp,
 125 | 			password: "changed-password",
 126 | 		});
 127 | 		const { data } = await client.signIn.email({
 128 | 			email: testUser.email,
 129 | 			password: "changed-password",
 130 | 		});
 131 | 		expect(data?.user).toBeDefined();
 132 | 	});
 133 | 
 134 | 	it("should call onPasswordReset callback when resetting password", async () => {
 135 | 		const onPasswordResetMock = vi.fn();
 136 | 		const { client, testUser } = await getTestInstance(
 137 | 			{
 138 | 				plugins: [
 139 | 					bearer(),
 140 | 					emailOTP({
 141 | 						async sendVerificationOTP({ email, otp: _otp, type }) {
 142 | 							otp = _otp;
 143 | 							otpFn(email, _otp, type);
 144 | 						},
 145 | 						sendVerificationOnSignUp: true,
 146 | 					}),
 147 | 				],
 148 | 				emailAndPassword: {
 149 | 					enabled: true,
 150 | 					onPasswordReset: onPasswordResetMock,
 151 | 				},
 152 | 			},
 153 | 			{
 154 | 				clientOptions: {
 155 | 					plugins: [emailOTPClient()],
 156 | 				},
 157 | 			},
 158 | 		);
 159 | 
 160 | 		await client.emailOtp.sendVerificationOtp({
 161 | 			email: testUser.email,
 162 | 			type: "forget-password",
 163 | 		});
 164 | 
 165 | 		await client.emailOtp.resetPassword({
 166 | 			email: testUser.email,
 167 | 			otp,
 168 | 			password: "new-password",
 169 | 		});
 170 | 
 171 | 		expect(onPasswordResetMock).toHaveBeenCalledWith(
 172 | 			{ user: expect.objectContaining({ email: testUser.email }) },
 173 | 			expect.any(Object),
 174 | 		);
 175 | 	});
 176 | 
 177 | 	it("should reset password and create credential account", async () => {
 178 | 		const testUser2 = {
 179 | 			email: "[email protected]",
 180 | 		};
 181 | 		await client.emailOtp.sendVerificationOtp({
 182 | 			email: testUser2.email,
 183 | 			type: "sign-in",
 184 | 		});
 185 | 		await client.signIn.emailOtp(
 186 | 			{
 187 | 				email: testUser2.email,
 188 | 				otp,
 189 | 			},
 190 | 			{
 191 | 				onSuccess: (ctx) => {
 192 | 					const header = ctx.response.headers.get("set-cookie");
 193 | 					expect(header).toContain("better-auth.session_token");
 194 | 				},
 195 | 			},
 196 | 		);
 197 | 		await client.emailOtp.sendVerificationOtp({
 198 | 			email: testUser2.email,
 199 | 			type: "forget-password",
 200 | 		});
 201 | 		await client.emailOtp.resetPassword({
 202 | 			email: testUser2.email,
 203 | 			otp,
 204 | 			password: "password",
 205 | 		});
 206 | 		const res = await client.signIn.email({
 207 | 			email: testUser2.email,
 208 | 			password: "password",
 209 | 		});
 210 | 		expect(res.data?.token).toBeDefined();
 211 | 	});
 212 | 
 213 | 	it("should fail on invalid email", async () => {
 214 | 		const res = await client.emailOtp.sendVerificationOtp({
 215 | 			email: "invalid-email",
 216 | 			type: "email-verification",
 217 | 		});
 218 | 		expect(res.error?.status).toBe(400);
 219 | 		expect(res.error?.code).toBe("INVALID_EMAIL");
 220 | 	});
 221 | 
 222 | 	it("should fail on expired otp", async () => {
 223 | 		await client.emailOtp.sendVerificationOtp({
 224 | 			email: testUser.email,
 225 | 			type: "email-verification",
 226 | 		});
 227 | 		vi.useFakeTimers();
 228 | 		await vi.advanceTimersByTimeAsync(1000 * 60 * 6);
 229 | 		const res = await client.emailOtp.verifyEmail({
 230 | 			email: testUser.email,
 231 | 			otp,
 232 | 		});
 233 | 		expect(res.error?.status).toBe(400);
 234 | 		expect(res.error?.code).toBe("OTP_EXPIRED");
 235 | 	});
 236 | 
 237 | 	it("should not fail on time elapsed", async () => {
 238 | 		await client.emailOtp.sendVerificationOtp({
 239 | 			email: testUser.email,
 240 | 			type: "email-verification",
 241 | 		});
 242 | 		vi.useFakeTimers();
 243 | 		await vi.advanceTimersByTimeAsync(1000 * 60 * 4);
 244 | 		const res = await client.emailOtp.verifyEmail({
 245 | 			email: testUser.email,
 246 | 			otp,
 247 | 		});
 248 | 		const session = await client.getSession({
 249 | 			fetchOptions: {
 250 | 				headers: {
 251 | 					Authorization: `Bearer ${res.data?.token}`,
 252 | 				},
 253 | 			},
 254 | 		});
 255 | 		expect(res.data?.status).toBe(true);
 256 | 		expect(session.data?.user.emailVerified).toBe(true);
 257 | 	});
 258 | 
 259 | 	it("should create verification otp on server", async () => {
 260 | 		otp = await auth.api.createVerificationOTP({
 261 | 			body: {
 262 | 				type: "sign-in",
 263 | 				email: "[email protected]",
 264 | 			},
 265 | 		});
 266 | 		otp = await auth.api.createVerificationOTP({
 267 | 			body: {
 268 | 				type: "sign-in",
 269 | 				email: "[email protected]",
 270 | 			},
 271 | 		});
 272 | 		expect(otp.length).toBe(6);
 273 | 	});
 274 | 
 275 | 	it("should get verification otp on server", async () => {
 276 | 		const res = await auth.api.getVerificationOTP({
 277 | 			query: {
 278 | 				email: "[email protected]",
 279 | 				type: "sign-in",
 280 | 			},
 281 | 		});
 282 | 	});
 283 | 
 284 | 	it("should work with custom options", async () => {
 285 | 		const { client, testUser, auth } = await getTestInstance(
 286 | 			{
 287 | 				plugins: [
 288 | 					bearer(),
 289 | 					emailOTP({
 290 | 						async sendVerificationOTP({ email, otp: _otp, type }) {
 291 | 							otp = _otp;
 292 | 							otpFn(email, _otp, type);
 293 | 						},
 294 | 						sendVerificationOnSignUp: true,
 295 | 						expiresIn: 10,
 296 | 						otpLength: 8,
 297 | 					}),
 298 | 				],
 299 | 				emailVerification: {
 300 | 					autoSignInAfterVerification: true,
 301 | 				},
 302 | 			},
 303 | 			{
 304 | 				clientOptions: {
 305 | 					plugins: [emailOTPClient()],
 306 | 				},
 307 | 			},
 308 | 		);
 309 | 		await client.emailOtp.sendVerificationOtp({
 310 | 			type: "email-verification",
 311 | 			email: testUser.email,
 312 | 		});
 313 | 		expect(otp.length).toBe(8);
 314 | 		vi.useFakeTimers();
 315 | 		await vi.advanceTimersByTimeAsync(11 * 1000);
 316 | 		const verifyRes = await client.emailOtp.verifyEmail({
 317 | 			email: testUser.email,
 318 | 			otp,
 319 | 		});
 320 | 		expect(verifyRes.error?.code).toBe("OTP_EXPIRED");
 321 | 	});
 322 | });
 323 | 
 324 | describe("email-otp-verify", async () => {
 325 | 	const otpFn = vi.fn();
 326 | 	const otp = [""];
 327 | 	const { client, testUser, auth } = await getTestInstance(
 328 | 		{
 329 | 			plugins: [
 330 | 				emailOTP({
 331 | 					async sendVerificationOTP({ email, otp: _otp, type }) {
 332 | 						otp.push(_otp);
 333 | 						otpFn(email, _otp, type);
 334 | 					},
 335 | 					sendVerificationOnSignUp: true,
 336 | 					disableSignUp: true,
 337 | 				}),
 338 | 			],
 339 | 		},
 340 | 		{
 341 | 			clientOptions: {
 342 | 				plugins: [emailOTPClient()],
 343 | 			},
 344 | 		},
 345 | 	);
 346 | 
 347 | 	it("should return USER_NOT_FOUND error when disableSignUp and user not registered", async () => {
 348 | 		const response = await client.emailOtp.sendVerificationOtp({
 349 | 			email: "[email protected]",
 350 | 			type: "email-verification",
 351 | 		});
 352 | 
 353 | 		expect(response.error?.message).toBe("User not found");
 354 | 		// Existing user should still succeed
 355 | 		const successRes = await client.emailOtp.sendVerificationOtp({
 356 | 			email: testUser.email,
 357 | 			type: "email-verification",
 358 | 		});
 359 | 		expect(successRes.error).toBeFalsy();
 360 | 	});
 361 | 
 362 | 	it("should verify email with last otp", async () => {
 363 | 		await client.emailOtp.sendVerificationOtp({
 364 | 			email: testUser.email,
 365 | 			type: "email-verification",
 366 | 		});
 367 | 		await client.emailOtp.sendVerificationOtp({
 368 | 			email: testUser.email,
 369 | 			type: "email-verification",
 370 | 		});
 371 | 		await client.emailOtp.sendVerificationOtp({
 372 | 			email: testUser.email,
 373 | 			type: "email-verification",
 374 | 		});
 375 | 	});
 376 | 
 377 | 	it("should block after exceeding allowed attempts", async () => {
 378 | 		await client.emailOtp.sendVerificationOtp({
 379 | 			email: testUser.email,
 380 | 			type: "email-verification",
 381 | 		});
 382 | 
 383 | 		for (let i = 0; i < 3; i++) {
 384 | 			const res = await client.emailOtp.verifyEmail({
 385 | 				email: testUser.email,
 386 | 				otp: "wrong-otp",
 387 | 			});
 388 | 			expect(res.error?.status).toBe(400);
 389 | 			expect(res.error?.message).toBe("Invalid OTP");
 390 | 		}
 391 | 
 392 | 		//Try one more time - should be blocked
 393 | 		const res = await client.emailOtp.verifyEmail({
 394 | 			email: testUser.email,
 395 | 			otp: "000000",
 396 | 		});
 397 | 		expect(res.error?.status).toBe(403);
 398 | 		expect(res.error?.message).toBe("Too many attempts");
 399 | 	});
 400 | 
 401 | 	it("should block reset password after exceeding allowed attempts", async () => {
 402 | 		await client.emailOtp.sendVerificationOtp({
 403 | 			email: testUser.email,
 404 | 			type: "forget-password",
 405 | 		});
 406 | 
 407 | 		for (let i = 0; i < 3; i++) {
 408 | 			const res = await client.emailOtp.resetPassword({
 409 | 				email: testUser.email,
 410 | 				otp: "wrong-otp",
 411 | 				password: "new-password",
 412 | 			});
 413 | 			expect(res.error?.status).toBe(400);
 414 | 			expect(res.error?.message).toBe("Invalid OTP");
 415 | 		}
 416 | 
 417 | 		// Try one more time - should be blocked
 418 | 		const res = await client.emailOtp.resetPassword({
 419 | 			email: testUser.email,
 420 | 			otp: "000000",
 421 | 			password: "new-password",
 422 | 		});
 423 | 		expect(res.error?.status).toBe(403);
 424 | 		expect(res.error?.message).toBe("Too many attempts");
 425 | 	});
 426 | });
 427 | 
 428 | describe("custom rate limiting storage", async () => {
 429 | 	const { client, testUser } = await getTestInstance({
 430 | 		rateLimit: {
 431 | 			enabled: true,
 432 | 		},
 433 | 		plugins: [
 434 | 			emailOTP({
 435 | 				async sendVerificationOTP(data, request) {},
 436 | 			}),
 437 | 		],
 438 | 	});
 439 | 
 440 | 	it.each([
 441 | 		{
 442 | 			path: "/email-otp/send-verification-otp",
 443 | 			body: {
 444 | 				email: "[email protected]",
 445 | 				type: "sign-in",
 446 | 			},
 447 | 		},
 448 | 		{
 449 | 			path: "/sign-in/email-otp",
 450 | 			body: {
 451 | 				email: "[email protected]",
 452 | 				otp: "12312",
 453 | 			},
 454 | 		},
 455 | 		{
 456 | 			path: "/email-otp/verify-email",
 457 | 			body: {
 458 | 				email: "[email protected]",
 459 | 				otp: "12312",
 460 | 			},
 461 | 		},
 462 | 	])("should rate limit send verification endpoint", async ({ path, body }) => {
 463 | 		for (let i = 0; i < 10; i++) {
 464 | 			const response = await client.$fetch(path, {
 465 | 				method: "POST",
 466 | 				body,
 467 | 			});
 468 | 			if (i >= 3) {
 469 | 				expect(response.error?.status).toBe(429);
 470 | 			}
 471 | 		}
 472 | 		vi.useFakeTimers();
 473 | 		await vi.advanceTimersByTimeAsync(60 * 1000);
 474 | 		const response = await client.$fetch(path, {
 475 | 			method: "POST",
 476 | 			body,
 477 | 		});
 478 | 		expect(response.error?.status).not.toBe(429);
 479 | 	});
 480 | });
 481 | 
 482 | describe("custom generate otpFn", async () => {
 483 | 	const { client, testUser } = await getTestInstance(
 484 | 		{
 485 | 			plugins: [
 486 | 				emailOTP({
 487 | 					async sendVerificationOTP(data, request) {},
 488 | 					generateOTP(data, request) {
 489 | 						return "123456";
 490 | 					},
 491 | 				}),
 492 | 			],
 493 | 		},
 494 | 		{
 495 | 			clientOptions: {
 496 | 				plugins: [emailOTPClient()],
 497 | 			},
 498 | 		},
 499 | 	);
 500 | 
 501 | 	it("should generate otp", async () => {
 502 | 		const res = await client.emailOtp.sendVerificationOtp({
 503 | 			email: testUser.email,
 504 | 			type: "email-verification",
 505 | 		});
 506 | 		expect(res.data?.success).toBe(true);
 507 | 	});
 508 | 
 509 | 	it("should verify email with otp", async () => {
 510 | 		const res = await client.emailOtp.verifyEmail({
 511 | 			email: testUser.email,
 512 | 			otp: "123456",
 513 | 		});
 514 | 		expect(res.data?.status).toBe(true);
 515 | 	});
 516 | });
 517 | 
 518 | describe("custom storeOTP", async () => {
 519 | 	// Testing hashed OTPs.
 520 | 	describe("hashed", async () => {
 521 | 		let sendVerificationOtpFn = async (data: {
 522 | 			email: string;
 523 | 			otp: string;
 524 | 			type: "sign-in" | "email-verification" | "forget-password";
 525 | 		}) => {};
 526 | 
 527 | 		function getTheSentOTP() {
 528 | 			let gotOtp: string | null = null;
 529 | 			let sub = (otp: string) => {};
 530 | 			sendVerificationOtpFn = async (data) => {
 531 | 				gotOtp = data.otp;
 532 | 				sub(data.otp);
 533 | 			};
 534 | 			return {
 535 | 				get: () =>
 536 | 					new Promise<string>((resolve) => {
 537 | 						if (gotOtp) {
 538 | 							resolve(gotOtp);
 539 | 						} else {
 540 | 							sub = (otp) => {
 541 | 								gotOtp = otp;
 542 | 								resolve(otp);
 543 | 							};
 544 | 						}
 545 | 					}),
 546 | 			};
 547 | 		}
 548 | 
 549 | 		const { client, testUser, auth } = await getTestInstance(
 550 | 			{
 551 | 				plugins: [
 552 | 					emailOTP({
 553 | 						sendVerificationOTP: async (d) => {
 554 | 							await sendVerificationOtpFn(d);
 555 | 						},
 556 | 						storeOTP: "hashed",
 557 | 					}),
 558 | 				],
 559 | 			},
 560 | 			{
 561 | 				clientOptions: {
 562 | 					plugins: [emailOTPClient()],
 563 | 				},
 564 | 			},
 565 | 		);
 566 | 		const authCtx = await auth.$context;
 567 | 		const userEmail1 = `${crypto.randomUUID()}@email.com`;
 568 | 
 569 | 		let validOTP = "";
 570 | 
 571 | 		it("should create a hashed otp", async () => {
 572 | 			const { get } = getTheSentOTP();
 573 | 			await client.emailOtp.sendVerificationOtp({
 574 | 				email: userEmail1,
 575 | 				type: "sign-in",
 576 | 			});
 577 | 			const verificationValue =
 578 | 				await authCtx.internalAdapter.findVerificationValue(
 579 | 					`sign-in-otp-${userEmail1}`,
 580 | 				);
 581 | 
 582 | 			const storedOtp = verificationValue?.value || "";
 583 | 			const otp = await get();
 584 | 			validOTP = otp;
 585 | 			expect(storedOtp.length !== 0).toBe(true);
 586 | 			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
 587 | 			expect(storedOtp.endsWith(":0")).toBe(true);
 588 | 		});
 589 | 
 590 | 		it("should not be allowed to get otp if storeOTP is hashed", async () => {
 591 | 			try {
 592 | 				await auth.api.getVerificationOTP({
 593 | 					query: {
 594 | 						email: userEmail1,
 595 | 						type: "sign-in",
 596 | 					},
 597 | 				});
 598 | 			} catch (error: any) {
 599 | 				expect(error.statusCode).toBe(400);
 600 | 				expect(error.status).toBe("BAD_REQUEST");
 601 | 				expect(error.body.code).toBe(
 602 | 					"OTP_IS_HASHED_CANNOT_RETURN_THE_PLAIN_TEXT_OTP",
 603 | 				);
 604 | 				return;
 605 | 			}
 606 | 			// Should not reach here given the above should throw and thus return.
 607 | 			expect(true).toBe(false);
 608 | 		});
 609 | 
 610 | 		it("should be able to sign in with normal otp", async () => {
 611 | 			const res = await client.signIn.emailOtp({
 612 | 				email: userEmail1,
 613 | 				otp: validOTP,
 614 | 			});
 615 | 			expect(res.data?.user.email).toBe(userEmail1);
 616 | 			expect(res.data?.token).toBeDefined();
 617 | 		});
 618 | 	});
 619 | 
 620 | 	// Testing encrypted OTPs.
 621 | 	describe("encrypted", async () => {
 622 | 		let sendVerificationOtpFn = async (data: {
 623 | 			email: string;
 624 | 			otp: string;
 625 | 			type: "sign-in" | "email-verification" | "forget-password";
 626 | 		}) => {};
 627 | 
 628 | 		function getTheSentOTP() {
 629 | 			let gotOtp: string | null = null;
 630 | 			let sub = (otp: string) => {};
 631 | 			sendVerificationOtpFn = async (data) => {
 632 | 				gotOtp = data.otp;
 633 | 				sub(data.otp);
 634 | 			};
 635 | 			return {
 636 | 				get: () =>
 637 | 					new Promise<string>((resolve) => {
 638 | 						if (gotOtp) {
 639 | 							resolve(gotOtp);
 640 | 						} else {
 641 | 							sub = (otp) => {
 642 | 								gotOtp = otp;
 643 | 								resolve(otp);
 644 | 							};
 645 | 						}
 646 | 					}),
 647 | 			};
 648 | 		}
 649 | 
 650 | 		const { client, testUser, auth } = await getTestInstance(
 651 | 			{
 652 | 				plugins: [
 653 | 					emailOTP({
 654 | 						sendVerificationOTP: async (d) => {
 655 | 							await sendVerificationOtpFn(d);
 656 | 						},
 657 | 						storeOTP: "encrypted",
 658 | 					}),
 659 | 				],
 660 | 			},
 661 | 			{
 662 | 				clientOptions: {
 663 | 					plugins: [emailOTPClient()],
 664 | 				},
 665 | 			},
 666 | 		);
 667 | 		const authCtx = await auth.$context;
 668 | 		const userEmail1 = `${crypto.randomUUID()}@email.com`;
 669 | 
 670 | 		let encryptedOtp = "";
 671 | 		let validOTP = "";
 672 | 
 673 | 		it("should create an encrypted otp", async () => {
 674 | 			const { get } = getTheSentOTP();
 675 | 			await client.emailOtp.sendVerificationOtp({
 676 | 				email: userEmail1,
 677 | 				type: "sign-in",
 678 | 			});
 679 | 			const verificationValue =
 680 | 				await authCtx.internalAdapter.findVerificationValue(
 681 | 					`sign-in-otp-${userEmail1}`,
 682 | 				);
 683 | 
 684 | 			const storedOtp = verificationValue?.value || "";
 685 | 			const otp = await get();
 686 | 			expect(storedOtp.length !== 0).toBe(true);
 687 | 			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
 688 | 			expect(storedOtp.endsWith(":0")).toBe(true);
 689 | 			encryptedOtp = storedOtp;
 690 | 			validOTP = otp;
 691 | 		});
 692 | 
 693 | 		it("should be allowed to get otp if storeOTP is encrypted", async () => {
 694 | 			try {
 695 | 				const res = await auth.api.getVerificationOTP({
 696 | 					query: {
 697 | 						email: userEmail1,
 698 | 						type: "sign-in",
 699 | 					},
 700 | 				});
 701 | 				if (!res.otp) {
 702 | 					expect(true).toBe(false);
 703 | 					return;
 704 | 				}
 705 | 				expect(res.otp).toEqual(validOTP);
 706 | 				expect(res.otp.length).toBe(6);
 707 | 			} catch (error: any) {
 708 | 				expect(error).not.toBeDefined();
 709 | 			}
 710 | 		});
 711 | 
 712 | 		it("should be able to sign in with encrypted otp", async () => {
 713 | 			const res = await client.signIn.emailOtp({
 714 | 				email: userEmail1,
 715 | 				otp: validOTP,
 716 | 			});
 717 | 			expect(res.data?.user.email).toBe(userEmail1);
 718 | 			expect(res.data?.token).toBeDefined();
 719 | 		});
 720 | 	});
 721 | 
 722 | 	describe("custom encryptor", async () => {
 723 | 		let sendVerificationOtpFn = async (data: {
 724 | 			email: string;
 725 | 			otp: string;
 726 | 			type: "sign-in" | "email-verification" | "forget-password";
 727 | 		}) => {};
 728 | 
 729 | 		function getTheSentOTP() {
 730 | 			let gotOtp: string | null = null;
 731 | 			let sub = (otp: string) => {};
 732 | 			sendVerificationOtpFn = async (data) => {
 733 | 				gotOtp = data.otp;
 734 | 				sub(data.otp);
 735 | 			};
 736 | 			return {
 737 | 				get: () =>
 738 | 					new Promise<string>((resolve) => {
 739 | 						if (gotOtp) {
 740 | 							resolve(gotOtp);
 741 | 						} else {
 742 | 							sub = (otp) => {
 743 | 								gotOtp = otp;
 744 | 								resolve(otp);
 745 | 							};
 746 | 						}
 747 | 					}),
 748 | 			};
 749 | 		}
 750 | 
 751 | 		const { client, testUser, auth } = await getTestInstance(
 752 | 			{
 753 | 				plugins: [
 754 | 					emailOTP({
 755 | 						sendVerificationOTP: async (d) => {
 756 | 							await sendVerificationOtpFn(d);
 757 | 						},
 758 | 						storeOTP: {
 759 | 							encrypt: async (otp) => {
 760 | 								return otp + "encrypted";
 761 | 							},
 762 | 							decrypt: async (otp) => {
 763 | 								return otp.replace("encrypted", "");
 764 | 							},
 765 | 						},
 766 | 					}),
 767 | 				],
 768 | 			},
 769 | 			{
 770 | 				clientOptions: {
 771 | 					plugins: [emailOTPClient()],
 772 | 				},
 773 | 			},
 774 | 		);
 775 | 		const authCtx = await auth.$context;
 776 | 
 777 | 		let validOTP = "";
 778 | 		let userEmail1 = `${crypto.randomUUID()}@email.com`;
 779 | 
 780 | 		it("should create a custom encryptor otp", async () => {
 781 | 			const { get } = getTheSentOTP();
 782 | 			await client.emailOtp.sendVerificationOtp({
 783 | 				email: userEmail1,
 784 | 				type: "sign-in",
 785 | 			});
 786 | 			const verificationValue =
 787 | 				await authCtx.internalAdapter.findVerificationValue(
 788 | 					`sign-in-otp-${userEmail1}`,
 789 | 				);
 790 | 			const storedOtp = verificationValue?.value || "";
 791 | 			const otp = await get();
 792 | 			expect(storedOtp.length !== 0).toBe(true);
 793 | 			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
 794 | 			expect(storedOtp.endsWith(":0")).toBe(true);
 795 | 			validOTP = otp;
 796 | 		});
 797 | 
 798 | 		it("should be allowed to get otp if storeOTP is custom encryptor", async () => {
 799 | 			try {
 800 | 				const res = await auth.api.getVerificationOTP({
 801 | 					query: {
 802 | 						email: userEmail1,
 803 | 						type: "sign-in",
 804 | 					},
 805 | 				});
 806 | 				if (!res.otp) {
 807 | 					expect(true).toBe(false);
 808 | 					return;
 809 | 				}
 810 | 				expect(res.otp).toEqual(validOTP);
 811 | 				expect(res.otp.length).toBe(6);
 812 | 			} catch (error: any) {
 813 | 				console.error(error);
 814 | 				expect(error).not.toBeDefined();
 815 | 			}
 816 | 		});
 817 | 
 818 | 		it("should be able to sign in with custom encryptor otp", async () => {
 819 | 			const res = await client.signIn.emailOtp({
 820 | 				email: userEmail1,
 821 | 				otp: validOTP,
 822 | 			});
 823 | 			expect(res.data?.user.email).toBe(userEmail1);
 824 | 			expect(res.data?.token).toBeDefined();
 825 | 		});
 826 | 	});
 827 | 
 828 | 	describe("custom hasher", async () => {
 829 | 		let sendVerificationOtpFn = async (data: {
 830 | 			email: string;
 831 | 			otp: string;
 832 | 			type: "sign-in" | "email-verification" | "forget-password";
 833 | 		}) => {};
 834 | 
 835 | 		function getTheSentOTP() {
 836 | 			let gotOtp: string | null = null;
 837 | 			let sub = (otp: string) => {};
 838 | 			sendVerificationOtpFn = async (data) => {
 839 | 				gotOtp = data.otp;
 840 | 				sub(data.otp);
 841 | 			};
 842 | 			return {
 843 | 				get: () =>
 844 | 					new Promise<string>((resolve) => {
 845 | 						if (gotOtp) {
 846 | 							resolve(gotOtp);
 847 | 						} else {
 848 | 							sub = (otp) => {
 849 | 								gotOtp = otp;
 850 | 								resolve(otp);
 851 | 							};
 852 | 						}
 853 | 					}),
 854 | 			};
 855 | 		}
 856 | 
 857 | 		const { client, testUser, auth } = await getTestInstance(
 858 | 			{
 859 | 				plugins: [
 860 | 					emailOTP({
 861 | 						sendVerificationOTP: async (d) => {
 862 | 							await sendVerificationOtpFn(d);
 863 | 						},
 864 | 						storeOTP: {
 865 | 							hash: async (otp) => {
 866 | 								return otp + "hashed";
 867 | 							},
 868 | 						},
 869 | 					}),
 870 | 				],
 871 | 			},
 872 | 			{
 873 | 				clientOptions: {
 874 | 					plugins: [emailOTPClient()],
 875 | 				},
 876 | 			},
 877 | 		);
 878 | 		const authCtx = await auth.$context;
 879 | 
 880 | 		let validOTP = "";
 881 | 		let userEmail1 = `${crypto.randomUUID()}@email.com`;
 882 | 
 883 | 		it("should create a custom hasher otp", async () => {
 884 | 			const { get } = getTheSentOTP();
 885 | 			await client.emailOtp.sendVerificationOtp({
 886 | 				email: userEmail1,
 887 | 				type: "sign-in",
 888 | 			});
 889 | 			const verificationValue =
 890 | 				await authCtx.internalAdapter.findVerificationValue(
 891 | 					`sign-in-otp-${userEmail1}`,
 892 | 				);
 893 | 			const storedOtp = verificationValue?.value || "";
 894 | 			const otp = await get();
 895 | 			expect(storedOtp.length !== 0).toBe(true);
 896 | 			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
 897 | 			expect(storedOtp.endsWith(":0")).toBe(true);
 898 | 			validOTP = otp;
 899 | 		});
 900 | 
 901 | 		it("should be allowed to get otp if storeOTP is custom hasher", async () => {
 902 | 			try {
 903 | 				const result = await auth.api.getVerificationOTP({
 904 | 					query: {
 905 | 						email: userEmail1,
 906 | 						type: "sign-in",
 907 | 					},
 908 | 				});
 909 | 			} catch (error: any) {
 910 | 				expect(error.statusCode).toBe(400);
 911 | 				expect(error.status).toBe("BAD_REQUEST");
 912 | 				expect(error.body.code).toBe(
 913 | 					"OTP_IS_HASHED_CANNOT_RETURN_THE_PLAIN_TEXT_OTP",
 914 | 				);
 915 | 				return;
 916 | 			}
 917 | 			// Should not reach here given the above should throw and thus return.
 918 | 			expect(true).toBe(false);
 919 | 		});
 920 | 
 921 | 		it("should be able to sign in with custom hasher otp", async () => {
 922 | 			const res = await client.signIn.emailOtp({
 923 | 				email: userEmail1,
 924 | 				otp: validOTP,
 925 | 			});
 926 | 			expect(res.data?.user.email).toBe(userEmail1);
 927 | 			expect(res.data?.token).toBeDefined();
 928 | 		});
 929 | 	});
 930 | });
 931 | 
 932 | describe("override default email verification", async () => {
 933 | 	let otp = "";
 934 | 	const { cookieSetter, customFetchImpl } = await getTestInstance({
 935 | 		emailAndPassword: {
 936 | 			enabled: true,
 937 | 		},
 938 | 		emailVerification: {
 939 | 			sendOnSignUp: true,
 940 | 		},
 941 | 		plugins: [
 942 | 			emailOTP({
 943 | 				async sendVerificationOTP(data, request) {
 944 | 					otp = data.otp;
 945 | 				},
 946 | 				overrideDefaultEmailVerification: true,
 947 | 			}),
 948 | 		],
 949 | 	});
 950 | 
 951 | 	const client = createAuthClient({
 952 | 		plugins: [emailOTPClient()],
 953 | 		baseURL: "http://localhost:3000",
 954 | 		fetchOptions: {
 955 | 			customFetchImpl,
 956 | 		},
 957 | 	});
 958 | 
 959 | 	const headers = new Headers();
 960 | 	it("should send verification email on sign up", async () => {
 961 | 		await client.signUp.email(
 962 | 			{
 963 | 				email: "[email protected]",
 964 | 				password: "password",
 965 | 				name: "Test User",
 966 | 			},
 967 | 			{
 968 | 				onSuccess: cookieSetter(headers),
 969 | 			},
 970 | 		);
 971 | 		expect(otp.length).toBe(6);
 972 | 	});
 973 | 
 974 | 	it("should verify email with otp", async () => {
 975 | 		const res = await client.emailOtp.verifyEmail({
 976 | 			email: "[email protected]",
 977 | 			otp,
 978 | 		});
 979 | 		expect(res.data?.status).toBe(true);
 980 | 		expect(res.data?.user.emailVerified).toBe(true);
 981 | 	});
 982 | 
 983 | 	it("should by default not override default email verification", async () => {
 984 | 		const sendVerificationOTP = vi.fn();
 985 | 		const { client } = await getTestInstance({
 986 | 			emailAndPassword: {
 987 | 				enabled: true,
 988 | 			},
 989 | 			emailVerification: {
 990 | 				sendOnSignUp: true,
 991 | 				async sendVerificationEmail(data, request) {
 992 | 					sendVerificationOTP(data, request);
 993 | 				},
 994 | 			},
 995 | 			plugins: [
 996 | 				emailOTP({
 997 | 					async sendVerificationOTP(data, request) {
 998 | 						//
 999 | 					},
1000 | 				}),
1001 | 			],
1002 | 		});
1003 | 		await client.signUp.email(
1004 | 			{
1005 | 				email: "[email protected]",
1006 | 				password: "password",
1007 | 				name: "Test User",
1008 | 			},
1009 | 			{
1010 | 				onSuccess: cookieSetter(headers),
1011 | 			},
1012 | 		);
1013 | 		expect(sendVerificationOTP).toHaveBeenCalled();
1014 | 	});
1015 | });
1016 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/device-authorization/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as z from "zod";
  2 | import { APIError } from "better-call";
  3 | import { createAuthEndpoint } from "@better-auth/core/middleware";
  4 | import type { InferOptionSchema } from "../../types/plugins";
  5 | import type { BetterAuthPlugin } from "@better-auth/core";
  6 | import { generateRandomString } from "../../crypto";
  7 | import { getSessionFromCtx } from "../../api/routes/session";
  8 | import { ms, type StringValue as MSStringValue } from "ms";
  9 | import { schema, type DeviceCode } from "./schema";
 10 | import { mergeSchema } from "../../db";
 11 | import { defineErrorCodes } from "@better-auth/core/utils";
 12 | 
 13 | const msStringValueSchema = z.custom<MSStringValue>(
 14 | 	(val) => {
 15 | 		try {
 16 | 			ms(val as MSStringValue);
 17 | 		} catch (e) {
 18 | 			return false;
 19 | 		}
 20 | 		return true;
 21 | 	},
 22 | 	{
 23 | 		message:
 24 | 			"Invalid time string format. Use formats like '30m', '5s', '1h', etc.",
 25 | 	},
 26 | );
 27 | 
 28 | export const $deviceAuthorizationOptionsSchema = z.object({
 29 | 	expiresIn: msStringValueSchema
 30 | 		.default("30m")
 31 | 		.describe(
 32 | 			"Time in seconds until the device code expires. Use formats like '30m', '5s', '1h', etc.",
 33 | 		),
 34 | 	interval: msStringValueSchema
 35 | 		.default("5s")
 36 | 		.describe(
 37 | 			"Time in seconds between polling attempts. Use formats like '30m', '5s', '1h', etc.",
 38 | 		),
 39 | 	deviceCodeLength: z
 40 | 		.number()
 41 | 		.int()
 42 | 		.positive()
 43 | 		.default(40)
 44 | 		.describe(
 45 | 			"Length of the device code to be generated. Default is 40 characters.",
 46 | 		),
 47 | 	userCodeLength: z
 48 | 		.number()
 49 | 		.int()
 50 | 		.positive()
 51 | 		.default(8)
 52 | 		.describe(
 53 | 			"Length of the user code to be generated. Default is 8 characters.",
 54 | 		),
 55 | 	generateDeviceCode: z
 56 | 		.custom<() => string | Promise<string>>(
 57 | 			(val) => typeof val === "function",
 58 | 			{
 59 | 				message:
 60 | 					"generateDeviceCode must be a function that returns a string or a promise that resolves to a string.",
 61 | 			},
 62 | 		)
 63 | 		.optional()
 64 | 		.describe(
 65 | 			"Function to generate a device code. If not provided, a default random string generator will be used.",
 66 | 		),
 67 | 	generateUserCode: z
 68 | 		.custom<() => string | Promise<string>>(
 69 | 			(val) => typeof val === "function",
 70 | 			{
 71 | 				message:
 72 | 					"generateUserCode must be a function that returns a string or a promise that resolves to a string.",
 73 | 			},
 74 | 		)
 75 | 		.optional()
 76 | 		.describe(
 77 | 			"Function to generate a user code. If not provided, a default random string generator will be used.",
 78 | 		),
 79 | 	validateClient: z
 80 | 		.custom<(clientId: string) => boolean | Promise<boolean>>(
 81 | 			(val) => typeof val === "function",
 82 | 			{
 83 | 				message:
 84 | 					"validateClient must be a function that returns a boolean or a promise that resolves to a boolean.",
 85 | 			},
 86 | 		)
 87 | 		.optional()
 88 | 		.describe(
 89 | 			"Function to validate the client ID. If not provided, no validation will be performed.",
 90 | 		),
 91 | 	onDeviceAuthRequest: z
 92 | 		.custom<
 93 | 			(clientId: string, scope: string | undefined) => void | Promise<void>
 94 | 		>((val) => typeof val === "function", {
 95 | 			message:
 96 | 				"onDeviceAuthRequest must be a function that returns void or a promise that resolves to void.",
 97 | 		})
 98 | 		.optional()
 99 | 		.describe(
100 | 			"Function to handle device authorization requests. If not provided, no additional actions will be taken.",
101 | 		),
102 | 	schema: z.custom<InferOptionSchema<typeof schema>>(() => true),
103 | });
104 | 
105 | /**
106 |  * @see {$deviceAuthorizationOptionsSchema}
107 |  */
108 | export type DeviceAuthorizationOptions = {
109 | 	expiresIn: MSStringValue;
110 | 	interval: MSStringValue;
111 | 	deviceCodeLength: number;
112 | 	userCodeLength: number;
113 | 	generateDeviceCode?: () => string | Promise<string>;
114 | 	generateUserCode?: () => string | Promise<string>;
115 | 	validateClient?: (clientId: string) => boolean | Promise<boolean>;
116 | 	onDeviceAuthRequest?: (
117 | 		clientId: string,
118 | 		scope: string | undefined,
119 | 	) => void | Promise<void>;
120 | 	schema?: InferOptionSchema<typeof schema>;
121 | };
122 | 
123 | export { deviceAuthorizationClient } from "./client";
124 | 
125 | const DEVICE_AUTHORIZATION_ERROR_CODES = defineErrorCodes({
126 | 	INVALID_DEVICE_CODE: "Invalid device code",
127 | 	EXPIRED_DEVICE_CODE: "Device code has expired",
128 | 	EXPIRED_USER_CODE: "User code has expired",
129 | 	AUTHORIZATION_PENDING: "Authorization pending",
130 | 	ACCESS_DENIED: "Access denied",
131 | 	INVALID_USER_CODE: "Invalid user code",
132 | 	DEVICE_CODE_ALREADY_PROCESSED: "Device code already processed",
133 | 	POLLING_TOO_FREQUENTLY: "Polling too frequently",
134 | 	USER_NOT_FOUND: "User not found",
135 | 	FAILED_TO_CREATE_SESSION: "Failed to create session",
136 | 	INVALID_DEVICE_CODE_STATUS: "Invalid device code status",
137 | 	AUTHENTICATION_REQUIRED: "Authentication required",
138 | });
139 | 
140 | const defaultCharset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
141 | 
142 | /**
143 |  * @internal
144 |  */
145 | const defaultGenerateDeviceCode = (length: number) => {
146 | 	return generateRandomString(length, "a-z", "A-Z", "0-9");
147 | };
148 | 
149 | /**
150 |  * @internal
151 |  */
152 | const defaultGenerateUserCode = (length: number) => {
153 | 	const chars = new Uint8Array(length);
154 | 	return Array.from(crypto.getRandomValues(chars))
155 | 		.map((byte) => defaultCharset[byte % defaultCharset.length])
156 | 		.join("");
157 | };
158 | 
159 | export const deviceAuthorization = (
160 | 	options: Partial<DeviceAuthorizationOptions> = {},
161 | ) => {
162 | 	const opts = $deviceAuthorizationOptionsSchema.parse(options);
163 | 	const generateDeviceCode = async () => {
164 | 		if (opts.generateDeviceCode) {
165 | 			return opts.generateDeviceCode();
166 | 		}
167 | 		return defaultGenerateDeviceCode(opts.deviceCodeLength);
168 | 	};
169 | 
170 | 	const generateUserCode = async () => {
171 | 		if (opts.generateUserCode) {
172 | 			return opts.generateUserCode();
173 | 		}
174 | 		return defaultGenerateUserCode(opts.userCodeLength);
175 | 	};
176 | 
177 | 	return {
178 | 		id: "device-authorization",
179 | 		schema: mergeSchema(schema, options?.schema),
180 | 		endpoints: {
181 | 			deviceCode: createAuthEndpoint(
182 | 				"/device/code",
183 | 				{
184 | 					method: "POST",
185 | 					body: z.object({
186 | 						client_id: z.string().meta({
187 | 							description: "The client ID of the application",
188 | 						}),
189 | 						scope: z
190 | 							.string()
191 | 							.meta({
192 | 								description: "Space-separated list of scopes",
193 | 							})
194 | 							.optional(),
195 | 					}),
196 | 					error: z.object({
197 | 						error: z.enum(["invalid_request", "invalid_client"]).meta({
198 | 							description: "Error code",
199 | 						}),
200 | 						error_description: z.string().meta({
201 | 							description: "Detailed error description",
202 | 						}),
203 | 					}),
204 | 					metadata: {
205 | 						openapi: {
206 | 							description: `Request a device and user code
207 | 
208 | Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2)`,
209 | 							responses: {
210 | 								200: {
211 | 									description: "Success",
212 | 									content: {
213 | 										"application/json": {
214 | 											schema: {
215 | 												type: "object",
216 | 												properties: {
217 | 													device_code: {
218 | 														type: "string",
219 | 														description: "The device verification code",
220 | 													},
221 | 													user_code: {
222 | 														type: "string",
223 | 														description: "The user code to display",
224 | 													},
225 | 													verification_uri: {
226 | 														type: "string",
227 | 														description: "The URL for user verification",
228 | 													},
229 | 													verification_uri_complete: {
230 | 														type: "string",
231 | 														description: "The complete URL with user code",
232 | 													},
233 | 													expires_in: {
234 | 														type: "number",
235 | 														description:
236 | 															"Lifetime in seconds of the device code",
237 | 													},
238 | 													interval: {
239 | 														type: "number",
240 | 														description: "Minimum polling interval in seconds",
241 | 													},
242 | 												},
243 | 											},
244 | 										},
245 | 									},
246 | 								},
247 | 								400: {
248 | 									description: "Error response",
249 | 									content: {
250 | 										"application/json": {
251 | 											schema: {
252 | 												type: "object",
253 | 												properties: {
254 | 													error: {
255 | 														type: "string",
256 | 														enum: ["invalid_request", "invalid_client"],
257 | 													},
258 | 													error_description: {
259 | 														type: "string",
260 | 													},
261 | 												},
262 | 											},
263 | 										},
264 | 									},
265 | 								},
266 | 							},
267 | 						},
268 | 					},
269 | 				},
270 | 				async (ctx) => {
271 | 					if (opts.validateClient) {
272 | 						const isValid = await opts.validateClient(ctx.body.client_id);
273 | 						if (!isValid) {
274 | 							throw new APIError("BAD_REQUEST", {
275 | 								error: "invalid_client",
276 | 								error_description: "Invalid client ID",
277 | 							});
278 | 						}
279 | 					}
280 | 
281 | 					if (opts.onDeviceAuthRequest) {
282 | 						await opts.onDeviceAuthRequest(ctx.body.client_id, ctx.body.scope);
283 | 					}
284 | 
285 | 					const deviceCode = await generateDeviceCode();
286 | 					const userCode = await generateUserCode();
287 | 					const expiresIn = ms(opts.expiresIn);
288 | 					const expiresAt = new Date(Date.now() + expiresIn);
289 | 
290 | 					await ctx.context.adapter.create({
291 | 						model: "deviceCode",
292 | 						data: {
293 | 							deviceCode,
294 | 							userCode,
295 | 							expiresAt,
296 | 							status: "pending",
297 | 							pollingInterval: ms(opts.interval),
298 | 							clientId: ctx.body.client_id,
299 | 							scope: ctx.body.scope,
300 | 						},
301 | 					});
302 | 
303 | 					const baseURL = new URL(ctx.context.baseURL);
304 | 					const verification_uri = new URL("/device", baseURL);
305 | 
306 | 					const verification_uri_complete = new URL(verification_uri);
307 | 					verification_uri_complete.searchParams.set(
308 | 						"user_code",
309 | 						// should we support custom formatting function here?
310 | 						encodeURIComponent(userCode),
311 | 					);
312 | 
313 | 					return ctx.json(
314 | 						{
315 | 							device_code: deviceCode,
316 | 							user_code: userCode,
317 | 							verification_uri: verification_uri.toString(),
318 | 							verification_uri_complete: verification_uri_complete.toString(),
319 | 							expires_in: Math.floor(expiresIn / 1000),
320 | 							interval: Math.floor(ms(opts.interval) / 1000),
321 | 						},
322 | 						{
323 | 							headers: {
324 | 								"Cache-Control": "no-store",
325 | 							},
326 | 						},
327 | 					);
328 | 				},
329 | 			),
330 | 			deviceToken: createAuthEndpoint(
331 | 				"/device/token",
332 | 				{
333 | 					method: "POST",
334 | 					body: z.object({
335 | 						grant_type: z
336 | 							.literal("urn:ietf:params:oauth:grant-type:device_code")
337 | 							.meta({
338 | 								description: "The grant type for device flow",
339 | 							}),
340 | 						device_code: z.string().meta({
341 | 							description: "The device verification code",
342 | 						}),
343 | 						client_id: z.string().meta({
344 | 							description: "The client ID of the application",
345 | 						}),
346 | 					}),
347 | 					error: z.object({
348 | 						error: z
349 | 							.enum([
350 | 								"authorization_pending",
351 | 								"slow_down",
352 | 								"expired_token",
353 | 								"access_denied",
354 | 								"invalid_request",
355 | 								"invalid_grant",
356 | 							])
357 | 							.meta({
358 | 								description: "Error code",
359 | 							}),
360 | 						error_description: z.string().meta({
361 | 							description: "Detailed error description",
362 | 						}),
363 | 					}),
364 | 					metadata: {
365 | 						openapi: {
366 | 							description: `Exchange device code for access token
367 | 
368 | Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#section-3.4)`,
369 | 							responses: {
370 | 								200: {
371 | 									description: "Success",
372 | 									content: {
373 | 										"application/json": {
374 | 											schema: {
375 | 												type: "object",
376 | 												properties: {
377 | 													session: {
378 | 														$ref: "#/components/schemas/Session",
379 | 													},
380 | 													user: {
381 | 														$ref: "#/components/schemas/User",
382 | 													},
383 | 												},
384 | 											},
385 | 										},
386 | 									},
387 | 								},
388 | 								400: {
389 | 									description: "Error response",
390 | 									content: {
391 | 										"application/json": {
392 | 											schema: {
393 | 												type: "object",
394 | 												properties: {
395 | 													error: {
396 | 														type: "string",
397 | 														enum: [
398 | 															"authorization_pending",
399 | 															"slow_down",
400 | 															"expired_token",
401 | 															"access_denied",
402 | 															"invalid_request",
403 | 															"invalid_grant",
404 | 														],
405 | 													},
406 | 													error_description: {
407 | 														type: "string",
408 | 													},
409 | 												},
410 | 											},
411 | 										},
412 | 									},
413 | 								},
414 | 							},
415 | 						},
416 | 					},
417 | 				},
418 | 				async (ctx) => {
419 | 					const { device_code, client_id } = ctx.body;
420 | 
421 | 					if (opts.validateClient) {
422 | 						const isValid = await opts.validateClient(client_id);
423 | 						if (!isValid) {
424 | 							throw new APIError("BAD_REQUEST", {
425 | 								error: "invalid_grant",
426 | 								error_description: "Invalid client ID",
427 | 							});
428 | 						}
429 | 					}
430 | 
431 | 					const deviceCodeRecord = await ctx.context.adapter.findOne<{
432 | 						id: string;
433 | 						deviceCode: string;
434 | 						userCode: string;
435 | 						userId?: string;
436 | 						expiresAt: Date;
437 | 						status: string;
438 | 						lastPolledAt?: Date;
439 | 						pollingInterval?: number;
440 | 						clientId?: string;
441 | 						scope?: string;
442 | 					}>({
443 | 						model: "deviceCode",
444 | 						where: [
445 | 							{
446 | 								field: "deviceCode",
447 | 								value: device_code,
448 | 							},
449 | 						],
450 | 					});
451 | 
452 | 					if (!deviceCodeRecord) {
453 | 						throw new APIError("BAD_REQUEST", {
454 | 							error: "invalid_grant",
455 | 							error_description:
456 | 								DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE,
457 | 						});
458 | 					}
459 | 
460 | 					if (
461 | 						deviceCodeRecord.clientId &&
462 | 						deviceCodeRecord.clientId !== client_id
463 | 					) {
464 | 						throw new APIError("BAD_REQUEST", {
465 | 							error: "invalid_grant",
466 | 							error_description: "Client ID mismatch",
467 | 						});
468 | 					}
469 | 
470 | 					// Check for rate limiting
471 | 					if (
472 | 						deviceCodeRecord.lastPolledAt &&
473 | 						deviceCodeRecord.pollingInterval
474 | 					) {
475 | 						const timeSinceLastPoll =
476 | 							Date.now() - new Date(deviceCodeRecord.lastPolledAt).getTime();
477 | 						const minInterval = deviceCodeRecord.pollingInterval;
478 | 
479 | 						if (timeSinceLastPoll < minInterval) {
480 | 							throw new APIError("BAD_REQUEST", {
481 | 								error: "slow_down",
482 | 								error_description:
483 | 									DEVICE_AUTHORIZATION_ERROR_CODES.POLLING_TOO_FREQUENTLY,
484 | 							});
485 | 						}
486 | 					}
487 | 
488 | 					// Update last polled time
489 | 					await ctx.context.adapter.update({
490 | 						model: "deviceCode",
491 | 						where: [
492 | 							{
493 | 								field: "id",
494 | 								value: deviceCodeRecord.id,
495 | 							},
496 | 						],
497 | 						update: {
498 | 							lastPolledAt: new Date(),
499 | 						},
500 | 					});
501 | 
502 | 					if (deviceCodeRecord.expiresAt < new Date()) {
503 | 						await ctx.context.adapter.delete({
504 | 							model: "deviceCode",
505 | 							where: [
506 | 								{
507 | 									field: "id",
508 | 									value: deviceCodeRecord.id,
509 | 								},
510 | 							],
511 | 						});
512 | 						throw new APIError("BAD_REQUEST", {
513 | 							error: "expired_token",
514 | 							error_description:
515 | 								DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_DEVICE_CODE,
516 | 						});
517 | 					}
518 | 
519 | 					if (deviceCodeRecord.status === "pending") {
520 | 						throw new APIError("BAD_REQUEST", {
521 | 							error: "authorization_pending",
522 | 							error_description:
523 | 								DEVICE_AUTHORIZATION_ERROR_CODES.AUTHORIZATION_PENDING,
524 | 						});
525 | 					}
526 | 
527 | 					if (deviceCodeRecord.status === "denied") {
528 | 						await ctx.context.adapter.delete({
529 | 							model: "deviceCode",
530 | 							where: [
531 | 								{
532 | 									field: "id",
533 | 									value: deviceCodeRecord.id,
534 | 								},
535 | 							],
536 | 						});
537 | 						throw new APIError("BAD_REQUEST", {
538 | 							error: "access_denied",
539 | 							error_description: DEVICE_AUTHORIZATION_ERROR_CODES.ACCESS_DENIED,
540 | 						});
541 | 					}
542 | 
543 | 					if (
544 | 						deviceCodeRecord.status === "approved" &&
545 | 						deviceCodeRecord.userId
546 | 					) {
547 | 						// Delete the device code after successful authorization
548 | 						await ctx.context.adapter.delete({
549 | 							model: "deviceCode",
550 | 							where: [
551 | 								{
552 | 									field: "id",
553 | 									value: deviceCodeRecord.id,
554 | 								},
555 | 							],
556 | 						});
557 | 
558 | 						const user = await ctx.context.internalAdapter.findUserById(
559 | 							deviceCodeRecord.userId,
560 | 						);
561 | 
562 | 						if (!user) {
563 | 							throw new APIError("INTERNAL_SERVER_ERROR", {
564 | 								error: "server_error",
565 | 								error_description:
566 | 									DEVICE_AUTHORIZATION_ERROR_CODES.USER_NOT_FOUND,
567 | 							});
568 | 						}
569 | 
570 | 						const session = await ctx.context.internalAdapter.createSession(
571 | 							user.id,
572 | 							ctx,
573 | 						);
574 | 
575 | 						if (!session) {
576 | 							throw new APIError("INTERNAL_SERVER_ERROR", {
577 | 								error: "server_error",
578 | 								error_description:
579 | 									DEVICE_AUTHORIZATION_ERROR_CODES.FAILED_TO_CREATE_SESSION,
580 | 							});
581 | 						}
582 | 
583 | 						// Set new session context for hooks and plugins
584 | 						// (matches setSessionCookie logic)
585 | 						ctx.context.setNewSession({
586 | 							session,
587 | 							user,
588 | 						});
589 | 
590 | 						// If secondary storage is enabled, store the session data in the secondary storage
591 | 						// (matches setSessionCookie logic)
592 | 						if (ctx.context.options.secondaryStorage) {
593 | 							await ctx.context.secondaryStorage?.set(
594 | 								session.token,
595 | 								JSON.stringify({
596 | 									user,
597 | 									session,
598 | 								}),
599 | 								Math.floor(
600 | 									(new Date(session.expiresAt).getTime() - Date.now()) / 1000,
601 | 								),
602 | 							);
603 | 						}
604 | 
605 | 						// Return OAuth 2.0 compliant token response
606 | 						return ctx.json(
607 | 							{
608 | 								access_token: session.token,
609 | 								token_type: "Bearer",
610 | 								expires_in: Math.floor(
611 | 									(new Date(session.expiresAt).getTime() - Date.now()) / 1000,
612 | 								),
613 | 								scope: deviceCodeRecord.scope || "",
614 | 							},
615 | 							{
616 | 								headers: {
617 | 									"Cache-Control": "no-store",
618 | 									Pragma: "no-cache",
619 | 								},
620 | 							},
621 | 						);
622 | 					}
623 | 
624 | 					throw new APIError("INTERNAL_SERVER_ERROR", {
625 | 						error: "server_error",
626 | 						error_description:
627 | 							DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE_STATUS,
628 | 					});
629 | 				},
630 | 			),
631 | 			deviceVerify: createAuthEndpoint(
632 | 				"/device",
633 | 				{
634 | 					method: "GET",
635 | 					query: z.object({
636 | 						user_code: z.string().meta({
637 | 							description: "The user code to verify",
638 | 						}),
639 | 					}),
640 | 					error: z.object({
641 | 						error: z.enum(["invalid_request"]).meta({
642 | 							description: "Error code",
643 | 						}),
644 | 						error_description: z.string().meta({
645 | 							description: "Detailed error description",
646 | 						}),
647 | 					}),
648 | 					metadata: {
649 | 						openapi: {
650 | 							description: "Display device verification page",
651 | 							responses: {
652 | 								200: {
653 | 									description: "Verification page HTML",
654 | 									content: {
655 | 										"application/json": {
656 | 											schema: {
657 | 												type: "object",
658 | 												properties: {
659 | 													user_code: {
660 | 														type: "string",
661 | 														description: "The user code to verify",
662 | 													},
663 | 													status: {
664 | 														type: "string",
665 | 														enum: ["pending", "approved", "denied"],
666 | 														description:
667 | 															"Current status of the device authorization",
668 | 													},
669 | 												},
670 | 											},
671 | 										},
672 | 									},
673 | 								},
674 | 							},
675 | 						},
676 | 					},
677 | 				},
678 | 				async (ctx) => {
679 | 					// This endpoint would typically serve an HTML page for user verification
680 | 					// For now, we'll return a simple JSON response
681 | 					const { user_code } = ctx.query;
682 | 					const cleanUserCode = user_code.replace(/-/g, "");
683 | 
684 | 					const deviceCodeRecord =
685 | 						await ctx.context.adapter.findOne<DeviceCode>({
686 | 							model: "deviceCode",
687 | 							where: [
688 | 								{
689 | 									field: "userCode",
690 | 									value: cleanUserCode,
691 | 								},
692 | 							],
693 | 						});
694 | 
695 | 					if (!deviceCodeRecord) {
696 | 						throw new APIError("BAD_REQUEST", {
697 | 							error: "invalid_request",
698 | 							error_description:
699 | 								DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE,
700 | 						});
701 | 					}
702 | 
703 | 					if (deviceCodeRecord.expiresAt < new Date()) {
704 | 						throw new APIError("BAD_REQUEST", {
705 | 							error: "expired_token",
706 | 							error_description:
707 | 								DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE,
708 | 						});
709 | 					}
710 | 
711 | 					return ctx.json({
712 | 						user_code: user_code,
713 | 						status: deviceCodeRecord.status,
714 | 					});
715 | 				},
716 | 			),
717 | 			deviceApprove: createAuthEndpoint(
718 | 				"/device/approve",
719 | 				{
720 | 					method: "POST",
721 | 					body: z.object({
722 | 						userCode: z.string().meta({
723 | 							description: "The user code to approve",
724 | 						}),
725 | 					}),
726 | 					error: z.object({
727 | 						error: z
728 | 							.enum([
729 | 								"invalid_request",
730 | 								"expired_token",
731 | 								"device_code_already_processed",
732 | 							])
733 | 							.meta({
734 | 								description: "Error code",
735 | 							}),
736 | 						error_description: z.string().meta({
737 | 							description: "Detailed error description",
738 | 						}),
739 | 					}),
740 | 					requireHeaders: true,
741 | 					metadata: {
742 | 						openapi: {
743 | 							description: "Approve device authorization",
744 | 							responses: {
745 | 								200: {
746 | 									description: "Success",
747 | 									content: {
748 | 										"application/json": {
749 | 											schema: {
750 | 												type: "object",
751 | 												properties: {
752 | 													success: {
753 | 														type: "boolean",
754 | 													},
755 | 												},
756 | 											},
757 | 										},
758 | 									},
759 | 								},
760 | 							},
761 | 						},
762 | 					},
763 | 				},
764 | 				async (ctx) => {
765 | 					const session = await getSessionFromCtx(ctx);
766 | 					if (!session) {
767 | 						throw new APIError("UNAUTHORIZED", {
768 | 							error: "unauthorized",
769 | 							error_description:
770 | 								DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED,
771 | 						});
772 | 					}
773 | 
774 | 					const { userCode } = ctx.body;
775 | 
776 | 					const deviceCodeRecord =
777 | 						await ctx.context.adapter.findOne<DeviceCode>({
778 | 							model: "deviceCode",
779 | 							where: [
780 | 								{
781 | 									field: "userCode",
782 | 									value: userCode,
783 | 								},
784 | 							],
785 | 						});
786 | 
787 | 					if (!deviceCodeRecord) {
788 | 						throw new APIError("BAD_REQUEST", {
789 | 							error: "invalid_request",
790 | 							error_description:
791 | 								DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE,
792 | 						});
793 | 					}
794 | 
795 | 					if (deviceCodeRecord.expiresAt < new Date()) {
796 | 						throw new APIError("BAD_REQUEST", {
797 | 							error: "expired_token",
798 | 							error_description:
799 | 								DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE,
800 | 						});
801 | 					}
802 | 
803 | 					if (deviceCodeRecord.status !== "pending") {
804 | 						throw new APIError("BAD_REQUEST", {
805 | 							error: "invalid_request",
806 | 							error_description:
807 | 								DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED,
808 | 						});
809 | 					}
810 | 
811 | 					// Update device code with approved status and user ID
812 | 					await ctx.context.adapter.update({
813 | 						model: "deviceCode",
814 | 						where: [
815 | 							{
816 | 								field: "id",
817 | 								value: deviceCodeRecord.id,
818 | 							},
819 | 						],
820 | 						update: {
821 | 							status: "approved",
822 | 							userId: session.user.id,
823 | 						},
824 | 					});
825 | 
826 | 					return ctx.json({
827 | 						success: true,
828 | 					});
829 | 				},
830 | 			),
831 | 			deviceDeny: createAuthEndpoint(
832 | 				"/device/deny",
833 | 				{
834 | 					method: "POST",
835 | 					body: z.object({
836 | 						userCode: z.string().meta({
837 | 							description: "The user code to deny",
838 | 						}),
839 | 					}),
840 | 					error: z.object({
841 | 						error: z.enum(["invalid_request", "expired_token"]).meta({
842 | 							description: "Error code",
843 | 						}),
844 | 						error_description: z.string().meta({
845 | 							description: "Detailed error description",
846 | 						}),
847 | 					}),
848 | 					metadata: {
849 | 						openapi: {
850 | 							description: "Deny device authorization",
851 | 							responses: {
852 | 								200: {
853 | 									description: "Success",
854 | 									content: {
855 | 										"application/json": {
856 | 											schema: {
857 | 												type: "object",
858 | 												properties: {
859 | 													success: {
860 | 														type: "boolean",
861 | 													},
862 | 												},
863 | 											},
864 | 										},
865 | 									},
866 | 								},
867 | 							},
868 | 						},
869 | 					},
870 | 				},
871 | 				async (ctx) => {
872 | 					const { userCode } = ctx.body;
873 | 					const cleanUserCode = userCode.replace(/-/g, "");
874 | 
875 | 					const deviceCodeRecord =
876 | 						await ctx.context.adapter.findOne<DeviceCode>({
877 | 							model: "deviceCode",
878 | 							where: [
879 | 								{
880 | 									field: "userCode",
881 | 									value: cleanUserCode,
882 | 								},
883 | 							],
884 | 						});
885 | 
886 | 					if (!deviceCodeRecord) {
887 | 						throw new APIError("BAD_REQUEST", {
888 | 							error: "invalid_request",
889 | 							error_description:
890 | 								DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE,
891 | 						});
892 | 					}
893 | 
894 | 					if (deviceCodeRecord.expiresAt < new Date()) {
895 | 						throw new APIError("BAD_REQUEST", {
896 | 							error: "expired_token",
897 | 							error_description:
898 | 								DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE,
899 | 						});
900 | 					}
901 | 
902 | 					if (deviceCodeRecord.status !== "pending") {
903 | 						throw new APIError("BAD_REQUEST", {
904 | 							error: "invalid_request",
905 | 							error_description:
906 | 								DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED,
907 | 						});
908 | 					}
909 | 
910 | 					// Update device code with denied status
911 | 					await ctx.context.adapter.update({
912 | 						model: "deviceCode",
913 | 						where: [
914 | 							{
915 | 								field: "id",
916 | 								value: deviceCodeRecord.id,
917 | 							},
918 | 						],
919 | 						update: {
920 | 							status: "denied",
921 | 						},
922 | 					});
923 | 
924 | 					return ctx.json({
925 | 						success: true,
926 | 					});
927 | 				},
928 | 			),
929 | 		},
930 | 		$ERROR_CODES: DEVICE_AUTHORIZATION_ERROR_CODES,
931 | 	} satisfies BetterAuthPlugin;
932 | };
933 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/guides/auth0-migration-guide.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Migrating from Auth0 to Better Auth
  3 | description: A step-by-step guide to transitioning from Auth0 to Better Auth.
  4 | ---
  5 | 
  6 | In this guide, we'll walk through the steps to migrate a project from Auth0 to Better Auth — including email/password with proper hashing, social/external accounts, two-factor authentication, and more.
  7 | 
  8 | <Callout type="warn">
  9 | This migration will invalidate all active sessions. This guide doesn't currently show you how to migrate Organizations but it should be possible with additional steps and the [Organization](/docs/plugins/organization) Plugin.
 10 | </Callout>
 11 | 
 12 | ## Before You Begin
 13 | 
 14 | Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started.
 15 | 
 16 | <Steps>
 17 | <Step>
 18 | ### Connect to your database
 19 | 
 20 | You'll need to connect to your database to migrate the users and accounts. You can use any database you want, but for this example, we'll use PostgreSQL.
 21 | 
 22 | ```package-install
 23 | npm install pg
 24 | ```
 25 | 
 26 | And then you can use the following code to connect to your database.
 27 | 
 28 | ```ts title="auth.ts"
 29 | import { Pool } from "pg";
 30 | 
 31 | export const auth = betterAuth({
 32 |     database: new Pool({ 
 33 |         connectionString: process.env.DATABASE_URL 
 34 |     }),
 35 | })
 36 | ```
 37 | </Step>
 38 | <Step>
 39 | ### Enable Email and Password (Optional)
 40 | 
 41 | Enable the email and password in your auth config and implement your own logic for sending verification emails, reset password emails, etc.
 42 | 
 43 | ```ts title="auth.ts"
 44 | import { betterAuth } from "better-auth";
 45 | 
 46 | export const auth = betterAuth({
 47 |     database: new Pool({ 
 48 |         connectionString: process.env.DATABASE_URL 
 49 |     }),
 50 |     emailAndPassword: { // [!code highlight]
 51 |         enabled: true, // [!code highlight]
 52 |     }, // [!code highlight]
 53 |     emailVerification: {
 54 |       sendVerificationEmail: async({ user, url })=>{
 55 |         // implement your logic here to send email verification
 56 |       }
 57 |     },
 58 | })
 59 | ```
 60 | 
 61 | See [Email and Password](/docs/authentication/email-password) for more configuration options.
 62 | </Step>
 63 | <Step>
 64 | ### Setup Social Providers (Optional)
 65 | 
 66 | Add social providers you have enabled in your Auth0 project in your auth config.
 67 | 
 68 | ```ts title="auth.ts"
 69 | import { betterAuth } from "better-auth";
 70 | 
 71 | export const auth = betterAuth({
 72 |     database: new Pool({ 
 73 |         connectionString: process.env.DATABASE_URL 
 74 |     }),
 75 |     emailAndPassword: { 
 76 |         enabled: true,
 77 |     },
 78 |     socialProviders: { // [!code highlight]
 79 |         google: { // [!code highlight]
 80 |             clientId: process.env.GOOGLE_CLIENT_ID, // [!code highlight]
 81 |             clientSecret: process.env.GOOGLE_CLIENT_SECRET, // [!code highlight]
 82 |         }, // [!code highlight]
 83 |         github: { // [!code highlight]
 84 |             clientId: process.env.GITHUB_CLIENT_ID, // [!code highlight]
 85 |             clientSecret: process.env.GITHUB_CLIENT_SECRET, // [!code highlight]
 86 |         } // [!code highlight]
 87 |     } // [!code highlight]
 88 | })
 89 | ```
 90 | </Step>
 91 | <Step>
 92 | ### Add Plugins (Optional)
 93 | 
 94 | You can add the following plugins to your auth config based on your needs.
 95 | 
 96 | [Admin](/docs/plugins/admin) Plugin will allow you to manage users, user impersonations and app level roles and permissions.
 97 | 
 98 | [Two Factor](/docs/plugins/2fa) Plugin will allow you to add two-factor authentication to your application.
 99 | 
100 | [Username](/docs/plugins/username) Plugin will allow you to add username authentication to your application.
101 | 
102 | ```ts title="auth.ts"
103 | import { Pool } from "pg";
104 | import { betterAuth } from "better-auth";
105 | import { admin, twoFactor, username } from "better-auth/plugins";
106 | 
107 | export const auth = betterAuth({
108 |     database: new Pool({ 
109 |         connectionString: process.env.DATABASE_URL 
110 |     }),
111 |     emailAndPassword: { 
112 |         enabled: true,
113 |         password: {
114 |             verify: (data) => {
115 |                 // this for an edgecase that you might run in to on verifying the password
116 |             }
117 |         }
118 |     },
119 |     socialProviders: {
120 |         google: {
121 |             clientId: process.env.GOOGLE_CLIENT_ID!,
122 |             clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
123 |         },
124 |         github: {
125 |             clientId: process.env.GITHUB_CLIENT_ID!,
126 |             clientSecret: process.env.GITHUB_CLIENT_SECRET!,
127 |         }
128 |     },
129 |     plugins: [admin(), twoFactor(), username()], // [!code highlight]
130 | })
131 | ```
132 | </Step>
133 | <Step>
134 | ### Generate Schema
135 | 
136 | If you're using a custom database adapter, generate the schema:
137 | 
138 | ```sh
139 | npx @better-auth/cli generate
140 | ```
141 | 
142 | or if you're using the default adapter, you can use the following command:
143 | 
144 | ```sh
145 | npx @better-auth/cli migrate
146 | ```
147 | </Step>
148 | <Step>
149 | ### Install Dependencies
150 | 
151 | Install the required dependencies for the migration:
152 | 
153 | ```bash
154 | npm install auth0
155 | ```
156 | </Step>
157 | <Step>
158 | ### Create the migration script
159 | 
160 | Create a new file called `migrate-auth0.ts` in the `scripts` folder and add the following code:
161 | 
162 | <Callout type="info">
163 | Instead of using the Management API, you can use Auth0's bulk user export functionality and pass the exported JSON data directly to the `auth0Users` array. This is especially useful if you need to migrate password hashes and complete user data, which are not available through the Management API.
164 | 
165 | **Important Notes:**
166 | - Password hashes export is only available for Auth0 Enterprise users
167 | - Free plan users cannot export password hashes and will need to request a support ticket
168 | - For detailed information about bulk user exports, see the [Auth0 Bulk User Export Documentation](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports)
169 | - For password hash export details, refer to [Exporting Password Hashes](https://auth0.com/docs/troubleshoot/customer-support/manage-subscriptions/export-data#user-passwords)
170 | 
171 | Example:
172 | ```ts
173 | // Replace this with your exported users JSON data
174 | const auth0Users = [
175 |   {
176 |     "email": "[email protected]",
177 |     "email_verified": false,
178 |     "name": "Hello world",
179 |     // Note: password_hash is only available for Enterprise users
180 |     "password_hash": "$2b$10$w4kfaZVjrcQ6ZOMiG.M8JeNvnVQkPKZV03pbDUHbxy9Ug0h/McDXi",
181 |     // ... other user data
182 |   }
183 | ];
184 | ```
185 | </Callout>
186 | 
187 | ```ts title="scripts/migrate-auth0.ts"
188 | import { ManagementClient } from 'auth0';
189 | import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
190 | import { auth } from '@/lib/auth';
191 | 
192 | const auth0Client = new ManagementClient({
193 |     domain: process.env.AUTH0_DOMAIN!,
194 |     clientId: process.env.AUTH0_CLIENT_ID!,
195 |     clientSecret: process.env.AUTH0_SECRET!,
196 | });
197 | 
198 | 
199 | 
200 | function safeDateConversion(timestamp?: string | number): Date {
201 |     if (!timestamp) return new Date();
202 | 
203 |     const numericTimestamp = typeof timestamp === 'string' ? Date.parse(timestamp) : timestamp;
204 | 
205 |     const milliseconds = numericTimestamp < 1000000000000 ? numericTimestamp * 1000 : numericTimestamp;
206 | 
207 |     const date = new Date(milliseconds);
208 | 
209 |     if (isNaN(date.getTime())) {
210 |         console.warn(`Invalid timestamp: ${timestamp}, falling back to current date`);
211 |         return new Date();
212 |     }
213 | 
214 |     // Check for unreasonable dates (before 2000 or after 2100)
215 |     const year = date.getFullYear();
216 |     if (year < 2000 || year > 2100) {
217 |         console.warn(`Suspicious date year: ${year}, falling back to current date`);
218 |         return new Date();
219 |     }
220 | 
221 |     return date;
222 | }
223 | 
224 | // Helper function to generate backup codes for 2FA
225 | async function generateBackupCodes(secret: string) {
226 |     const key = secret;
227 |     const backupCodes = Array.from({ length: 10 })
228 |         .fill(null)
229 |         .map(() => generateRandomString(10, "a-z", "0-9", "A-Z"))
230 |         .map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
231 | 
232 |     const encCodes = await symmetricEncrypt({
233 |         data: JSON.stringify(backupCodes),
234 |         key: key,
235 |     });
236 |     return encCodes;
237 | }
238 | 
239 | function mapAuth0RoleToBetterAuthRole(auth0Roles: string[]) {
240 |     if (typeof auth0Roles === 'string') return auth0Roles;
241 |     if (Array.isArray(auth0Roles)) return auth0Roles.join(',');
242 | }
243 | // helper function to migrate password from auth0 to better auth for custom hashes and algs
244 | async function migratePassword(auth0User: any) {
245 |     if (auth0User.password_hash) {
246 |         if (auth0User.password_hash.startsWith('$2a$') || auth0User.password_hash.startsWith('$2b$')) {
247 |             return auth0User.password_hash;
248 |         }
249 |     }
250 | 
251 |     if (auth0User.custom_password_hash) {
252 |         const customHash = auth0User.custom_password_hash;
253 | 
254 |         if (customHash.algorithm === 'bcrypt') {
255 |             const hash = customHash.hash.value;
256 |             if (hash.startsWith('$2a$') || hash.startsWith('$2b$')) {
257 |                 return hash;
258 |             }
259 |         }
260 | 
261 |         return JSON.stringify({
262 |             algorithm: customHash.algorithm,
263 |             hash: {
264 |                 value: customHash.hash.value,
265 |                 encoding: customHash.hash.encoding || 'utf8',
266 |                 ...(customHash.hash.digest && { digest: customHash.hash.digest }),
267 |                 ...(customHash.hash.key && {
268 |                     key: {
269 |                         value: customHash.hash.key.value,
270 |                         encoding: customHash.hash.key.encoding || 'utf8'
271 |                     }
272 |                 })
273 |             },
274 |             ...(customHash.salt && {
275 |                 salt: {
276 |                     value: customHash.salt.value,
277 |                     encoding: customHash.salt.encoding || 'utf8',
278 |                     position: customHash.salt.position || 'prefix'
279 |                 }
280 |             }),
281 |             ...(customHash.password && {
282 |                 password: {
283 |                     encoding: customHash.password.encoding || 'utf8'
284 |                 }
285 |             }),
286 |             ...(customHash.algorithm === 'scrypt' && {
287 |                 keylen: customHash.keylen,
288 |                 cost: customHash.cost || 16384,
289 |                 blockSize: customHash.blockSize || 8,
290 |                 parallelization: customHash.parallelization || 1
291 |             })
292 |         });
293 |     }
294 | 
295 |     return null;
296 | }
297 | 
298 | async function migrateMFAFactors(auth0User: any, userId: string | undefined, ctx: any) {
299 |     if (!userId || !auth0User.mfa_factors || !Array.isArray(auth0User.mfa_factors)) {
300 |         return;
301 |     }
302 | 
303 |     for (const factor of auth0User.mfa_factors) {
304 |         try {
305 |             if (factor.totp && factor.totp.secret) {
306 |                 await ctx.adapter.create({
307 |                     model: "twoFactor",
308 |                     data: {
309 |                         userId: userId,
310 |                         secret: factor.totp.secret,
311 |                         backupCodes: await generateBackupCodes(factor.totp.secret)
312 |                     }
313 |                 });
314 |             }
315 |         } catch (error) {
316 |             console.error(`Failed to migrate MFA factor for user ${userId}:`, error);
317 |         }
318 |     }
319 | }
320 | 
321 | async function migrateOAuthAccounts(auth0User: any, userId: string | undefined, ctx: any) {
322 |     if (!userId || !auth0User.identities || !Array.isArray(auth0User.identities)) {
323 |         return;
324 |     }
325 | 
326 |     for (const identity of auth0User.identities) {
327 |         try {
328 |             const providerId = identity.provider === 'auth0' ? "credential" : identity.provider.split("-")[0];
329 |             await ctx.adapter.create({
330 |                 model: "account",
331 |                 data: {
332 |                     id: `${auth0User.user_id}|${identity.provider}|${identity.user_id}`,
333 |                     userId: userId,
334 |                     password: await migratePassword(auth0User),
335 |                     providerId: providerId || identity.provider,
336 |                     accountId: identity.user_id,
337 |                     accessToken: identity.access_token,
338 |                     tokenType: identity.token_type,
339 |                     refreshToken: identity.refresh_token,
340 |                     accessTokenExpiresAt: identity.expires_in ? new Date(Date.now() + identity.expires_in * 1000) : undefined,
341 |                     // if you are enterprise user, you can get the refresh tokens or all the tokensets - auth0Client.users.getAllTokensets 
342 |                     refreshTokenExpiresAt: identity.refresh_token_expires_in ? new Date(Date.now() + identity.refresh_token_expires_in * 1000) : undefined,
343 | 
344 |                     scope: identity.scope,
345 |                     idToken: identity.id_token,
346 |                     createdAt: safeDateConversion(auth0User.created_at),
347 |                     updatedAt: safeDateConversion(auth0User.updated_at)
348 |                 },
349 |                 forceAllowId: true
350 |             }).catch((error: Error) => {
351 |                 console.error(`Failed to create OAuth account for user ${userId} with provider ${providerId}:`, error);
352 |                 return ctx.adapter.create({
353 |                     // Try creating without optional fields if the first attempt failed
354 |                     model: "account",
355 |                     data: {
356 |                         id: `${auth0User.user_id}|${identity.provider}|${identity.user_id}`,
357 |                         userId: userId,
358 |                         password: migratePassword(auth0User),
359 |                         providerId: providerId,
360 |                         accountId: identity.user_id,
361 |                         accessToken: identity.access_token,
362 |                         tokenType: identity.token_type,
363 |                         refreshToken: identity.refresh_token,
364 |                         accessTokenExpiresAt: identity.expires_in ? new Date(Date.now() + identity.expires_in * 1000) : undefined,
365 |                         refreshTokenExpiresAt: identity.refresh_token_expires_in ? new Date(Date.now() + identity.refresh_token_expires_in * 1000) : undefined,
366 |                         scope: identity.scope,
367 |                         idToken: identity.id_token,
368 |                         createdAt: safeDateConversion(auth0User.created_at),
369 |                         updatedAt: safeDateConversion(auth0User.updated_at)
370 |                     },
371 |                     forceAllowId: true
372 |                 });
373 |             });
374 | 
375 |             console.log(`Successfully migrated OAuth account for user ${userId} with provider ${providerId}`);
376 |         } catch (error) {
377 |             console.error(`Failed to migrate OAuth account for user ${userId}:`, error);
378 |         }
379 |     }
380 | }
381 | 
382 | async function migrateOrganizations(ctx: any) {
383 |     try {
384 |         const organizations = await auth0Client.organizations.getAll();
385 |         for (const org of organizations.data || []) {
386 |             try {
387 |                 await ctx.adapter.create({
388 |                     model: "organization",
389 |                     data: {
390 |                         id: org.id,
391 |                         name: org.display_name || org.id,
392 |                         slug: (org.display_name || org.id).toLowerCase().replace(/[^a-z0-9]/g, '-'),
393 |                         logo: org.branding?.logo_url,
394 |                         metadata: JSON.stringify(org.metadata || {}),
395 |                         createdAt: safeDateConversion(org.created_at),
396 |                     },
397 |                     forceAllowId: true
398 |                 });
399 |                 const members = await auth0Client.organizations.getMembers({ id: org.id });
400 |                 for (const member of members.data || []) {
401 |                     try {
402 |                         const userRoles = await auth0Client.organizations.getMemberRoles({
403 |                             id: org.id,
404 |                             user_id: member.user_id
405 |                         });
406 |                         const role = mapAuth0RoleToBetterAuthRole(userRoles.data?.map(r => r.name) || []);
407 |                         await ctx.adapter.create({
408 |                             model: "member",
409 |                             data: {
410 |                                 id: `${org.id}|${member.user_id}`,
411 |                                 organizationId: org.id,
412 |                                 userId: member.user_id,
413 |                                 role: role,
414 |                                 createdAt: new Date()
415 |                             },
416 |                             forceAllowId: true
417 |                         });
418 | 
419 |                         console.log(`Successfully migrated member ${member.user_id} for organization ${org.display_name || org.id}`);
420 |                     } catch (error) {
421 |                         console.error(`Failed to migrate member ${member.user_id} for organization ${org.display_name || org.id}:`, error);
422 |                     }
423 |                 }
424 | 
425 |                 console.log(`Successfully migrated organization: ${org.display_name || org.id}`);
426 |             } catch (error) {
427 |                 console.error(`Failed to migrate organization ${org.display_name || org.id}:`, error);
428 |             }
429 |         }
430 |         console.log('Organization migration completed');
431 |     } catch (error) {
432 |         console.error('Failed to migrate organizations:', error);
433 |     }
434 | }
435 | 
436 | async function migrateFromAuth0() {
437 |     try {
438 |         const ctx = await auth.$context;
439 |         const isAdminEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "admin");
440 |         const isUsernameEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "username");
441 |         const isOrganizationEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "organization");
442 |         const perPage = 100;
443 |         const auth0Users: any[] = [];
444 |         let pageNumber = 0;
445 | 
446 |         while (true) {
447 |             try {
448 |                 const params = {
449 |                     per_page: perPage,
450 |                     page: pageNumber,
451 |                     include_totals: true,
452 |                 };
453 |                 const response = (await auth0Client.users.getAll(params)).data as any;
454 |                 const users = response.users || [];
455 |                 if (users.length === 0) break;
456 |                 auth0Users.push(...users);
457 |                 pageNumber++;
458 | 
459 |                 if (users.length < perPage) break;
460 |             } catch (error) {
461 |                 console.error('Error fetching users:', error);
462 |                 break;
463 |             }
464 |         }
465 | 
466 | 
467 |         console.log(`Found ${auth0Users.length} users to migrate`);
468 | 
469 |         for (const auth0User of auth0Users) {
470 |             try {
471 |                 // Determine if this is a password-based or OAuth user
472 |                 const isOAuthUser = auth0User.identities?.some((identity: any) => identity.provider !== 'auth0');
473 |                 // Base user data that's common for both types
474 |                 const baseUserData = {
475 |                     id: auth0User.user_id,
476 |                     email: auth0User.email,
477 |                     emailVerified: auth0User.email_verified || false,
478 |                     name: auth0User.name || auth0User.nickname,
479 |                     image: auth0User.picture,
480 |                     createdAt: safeDateConversion(auth0User.created_at),
481 |                     updatedAt: safeDateConversion(auth0User.updated_at),
482 |                     ...(isAdminEnabled ? {
483 |                         banned: auth0User.blocked || false,
484 |                         role: mapAuth0RoleToBetterAuthRole(auth0User.roles || []),
485 |                     } : {}),
486 | 
487 |                     ...(isUsernameEnabled ? {
488 |                         username: auth0User.username || auth0User.nickname,
489 |                     } : {}),
490 | 
491 |                 };
492 | 
493 |                 const createdUser = await ctx.adapter.create({
494 |                     model: "user",
495 |                     data: {
496 |                         ...baseUserData,
497 |                     },
498 |                     forceAllowId: true
499 |                 });
500 | 
501 |                 if (!createdUser?.id) {
502 |                     throw new Error('Failed to create user');
503 |                 }
504 | 
505 | 
506 |                 await migrateOAuthAccounts(auth0User, createdUser.id, ctx)
507 |                 console.log(`Successfully migrated user: ${auth0User.email}`);
508 |             } catch (error) {
509 |                 console.error(`Failed to migrate user ${auth0User.email}:`, error);
510 |             }
511 |         }
512 |         if (isOrganizationEnabled) {
513 |             await migrateOrganizations(ctx);
514 |         }
515 |         // the reset of migration will be here.
516 |         console.log('Migration completed successfully');
517 |     } catch (error) {
518 |         console.error('Migration failed:', error);
519 |         throw error;
520 |     }
521 | }
522 | 
523 | migrateFromAuth0()
524 |     .then(() => {
525 |         console.log('Migration completed');
526 |         process.exit(0);
527 |     })
528 |     .catch((error) => {
529 |         console.error('Migration failed:', error);
530 |         process.exit(1);
531 |     }); 
532 | ```
533 | 
534 | Make sure to replace the Auth0 environment variables with your own values:
535 | - `AUTH0_DOMAIN`
536 | - `AUTH0_CLIENT_ID`
537 | - `AUTH0_SECRET`
538 | </Step>
539 | 
540 | <Step>
541 | ### Run the migration
542 | 
543 | Run the migration script:
544 | 
545 | ```sh
546 | bun run scripts/migrate-auth0.ts # or use your preferred runtime
547 | ```
548 | 
549 | <Callout type="warning">
550 | Important considerations:
551 | 1. Test the migration in a development environment first
552 | 2. Monitor the migration process for any errors
553 | 3. Verify the migrated data in Better Auth before proceeding
554 | 4. Keep Auth0 installed and configured until the migration is complete
555 | 5. The script handles bcrypt password hashes by default. For custom password hashing algorithms, you'll need to modify the `migratePassword` function
556 | </Callout>
557 | 
558 | </Step>
559 | 
560 | <Step>
561 |  ### Change password hashing algorithm
562 | 
563 |  By default, Better Auth uses the `scrypt` algorithm to hash passwords. Since Auth0 uses `bcrypt`, you'll need to configure Better Auth to use bcrypt for password verification.
564 | 
565 |  First, install bcrypt:
566 | 
567 |  ```bash
568 |  npm install bcrypt
569 |  npm install -D @types/bcrypt
570 |  ```
571 | 
572 |  Then update your auth configuration:
573 | 
574 |  ```ts title="auth.ts"
575 |  import { betterAuth } from "better-auth";
576 |  import bcrypt from "bcrypt";
577 |  
578 |  export const auth = betterAuth({
579 |     emailAndPassword: {
580 |         password: {
581 |             hash: async (password) => {
582 |                 return await bcrypt.hash(password, 10);
583 |             },
584 |             verify: async ({ hash, password }) => {
585 |                 return await bcrypt.compare(password, hash);
586 |             }
587 |         }
588 |     }
589 |  })
590 |  ```
591 | </Step>
592 | <Step>
593 | ### Verify the migration
594 | 
595 | After running the migration, verify that:
596 | 1. All users have been properly migrated
597 | 2. Social connections are working
598 | 3. Password-based authentication is working
599 | 4. Two-factor authentication settings are preserved (if enabled)
600 | 5. User roles and permissions are correctly mapped
601 | </Step>
602 | <Step>
603 | ### Update your components
604 | 
605 | Now that the data is migrated, update your components to use Better Auth. Here's an example for the sign-in component:
606 | 
607 | ```tsx title="components/auth/sign-in.tsx"
608 | import { authClient } from "better-auth/client";
609 | 
610 | export const SignIn = () => {
611 |   const handleSignIn = async () => {
612 |     const { data, error } = await authClient.signIn.email({
613 |       email: "[email protected]",
614 |       password: "helloworld",
615 |     });
616 |     
617 |     if (error) {
618 |       console.error(error);
619 |       return;
620 |     }
621 |     // Handle successful sign in
622 |   };
623 | 
624 |   return (
625 |     <form onSubmit={handleSignIn}>
626 |       <button type="submit">Sign in</button>
627 |     </form>
628 |   );
629 | };
630 | ```
631 | </Step>
632 | <Step>
633 | ### Update the middleware
634 | 
635 | Replace your Auth0 middleware with Better Auth's middleware:
636 | 
637 | ```ts title="middleware.ts"
638 | import { NextRequest, NextResponse } from "next/server";
639 | import { getSessionCookie } from "better-auth/cookies";
640 | 
641 | export async function middleware(request: NextRequest) {
642 |   const sessionCookie = getSessionCookie(request);
643 |   const { pathname } = request.nextUrl;
644 | 
645 |   if (sessionCookie && ["/login", "/signup"].includes(pathname)) {
646 |     return NextResponse.redirect(new URL("/dashboard", request.url));
647 |   }
648 | 
649 |   if (!sessionCookie && pathname.startsWith("/dashboard")) {
650 |     return NextResponse.redirect(new URL("/login", request.url));
651 |   }
652 | 
653 |   return NextResponse.next();
654 | }
655 | 
656 | export const config = {
657 |   matcher: ["/dashboard", "/login", "/signup"],
658 | };
659 | ```
660 | </Step>
661 | <Step>
662 | ### Remove Auth0 Dependencies
663 | 
664 | Once you've verified everything is working correctly with Better Auth, remove Auth0:
665 | 
666 | ```bash
667 | npm remove @auth0/auth0-react @auth0/auth0-spa-js @auth0/nextjs-auth0
668 | ```
669 | </Step>
670 | </Steps>
671 | 
672 | ## Additional Considerations
673 | 
674 | ### Password Migration
675 | The migration script handles bcrypt password hashes by default. If you're using custom password hashing algorithms in Auth0, you'll need to modify the `migratePassword` function in the migration script to handle your specific case.
676 | 
677 | ### Role Mapping
678 | The script includes a basic role mapping function (`mapAuth0RoleToBetterAuthRole`). Customize this function based on your Auth0 roles and Better Auth role requirements.
679 | 
680 | ### Rate Limiting
681 | The migration script includes pagination to handle large numbers of users. Adjust the `perPage` value based on your needs and Auth0's rate limits.
682 | 
683 | ## Wrapping Up
684 | 
685 | Now! You've successfully migrated from Auth0 to Better Auth.
686 | 
687 | Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential. 
```
Page 46/67FirstPrevNextLast