#
tokens: 43989/50000 3/1091 files (page 45/67)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 45 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/organization/routes/crud-access-control.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, expectTypeOf } from "vitest";
  2 | import { getTestInstance } from "../../../test-utils/test-instance";
  3 | import { organization } from "../organization";
  4 | import { createAuthClient } from "../../../client";
  5 | import { inferOrgAdditionalFields, organizationClient } from "../client";
  6 | import { createAccessControl } from "../../access";
  7 | import { adminAc, defaultStatements, memberAc, ownerAc } from "../access";
  8 | import { parseSetCookieHeader } from "../../../cookies";
  9 | import type { DBFieldAttribute } from "@better-auth/core/db";
 10 | import { ORGANIZATION_ERROR_CODES } from "../error-codes";
 11 | 
 12 | describe("dynamic access control", async (it) => {
 13 | 	const ac = createAccessControl({
 14 | 		project: ["create", "read", "update", "delete"],
 15 | 		sales: ["create", "read", "update", "delete"],
 16 | 		...defaultStatements,
 17 | 	});
 18 | 	const owner = ac.newRole({
 19 | 		project: ["create", "delete", "update", "read"],
 20 | 		sales: ["create", "read", "update", "delete"],
 21 | 		...ownerAc.statements,
 22 | 	});
 23 | 	const admin = ac.newRole({
 24 | 		project: ["create", "read", "delete", "update"],
 25 | 		sales: ["create", "read"],
 26 | 		...adminAc.statements,
 27 | 	});
 28 | 	const member = ac.newRole({
 29 | 		project: ["read"],
 30 | 		sales: ["read"],
 31 | 		...memberAc.statements,
 32 | 	});
 33 | 
 34 | 	const additionalFields = {
 35 | 		color: {
 36 | 			type: "string",
 37 | 			defaultValue: "#ffffff",
 38 | 			required: true,
 39 | 		},
 40 | 		serverOnlyValue: {
 41 | 			type: "string",
 42 | 			defaultValue: "server-only-value",
 43 | 			input: false,
 44 | 			required: true,
 45 | 		},
 46 | 	} satisfies Record<string, DBFieldAttribute>;
 47 | 
 48 | 	const { auth, customFetchImpl, sessionSetter, signInWithTestUser } =
 49 | 		await getTestInstance({
 50 | 			plugins: [
 51 | 				organization({
 52 | 					ac,
 53 | 					roles: {
 54 | 						admin,
 55 | 						member,
 56 | 						owner,
 57 | 					},
 58 | 					dynamicAccessControl: {
 59 | 						enabled: true,
 60 | 					},
 61 | 					schema: {
 62 | 						organizationRole: {
 63 | 							additionalFields,
 64 | 						},
 65 | 					},
 66 | 				}),
 67 | 			],
 68 | 		});
 69 | 
 70 | 	const authClient = createAuthClient({
 71 | 		baseURL: "http://localhost:3000",
 72 | 		plugins: [
 73 | 			organizationClient({
 74 | 				ac,
 75 | 				roles: {
 76 | 					admin,
 77 | 					member,
 78 | 					owner,
 79 | 				},
 80 | 				dynamicAccessControl: {
 81 | 					enabled: true,
 82 | 				},
 83 | 				schema: inferOrgAdditionalFields<typeof auth>(),
 84 | 			}),
 85 | 		],
 86 | 		fetchOptions: {
 87 | 			customFetchImpl,
 88 | 		},
 89 | 	});
 90 | 	const {
 91 | 		organization: { checkRolePermission, hasPermission, create },
 92 | 	} = authClient;
 93 | 
 94 | 	const { headers, user, session } = await signInWithTestUser();
 95 | 
 96 | 	async function createUser({ role }: { role: "admin" | "member" | "owner" }) {
 97 | 		const normalUserDetails = {
 98 | 			email: `some-test-user-${crypto.randomUUID()}@email.com`,
 99 | 			name: `some-test-user`,
100 | 			password: `some-test-user-${crypto.randomUUID()}`,
101 | 		};
102 | 		const normalUser = await auth.api.signUpEmail({ body: normalUserDetails });
103 | 		const member = await auth.api.addMember({
104 | 			body: {
105 | 				role: role || "member",
106 | 				userId: normalUser.user.id,
107 | 				organizationId: org.data?.id,
108 | 			},
109 | 			headers,
110 | 		});
111 | 		if (!member) throw new Error("Member not found");
112 | 		let userHeaders = new Headers();
113 | 		await authClient.signIn.email({
114 | 			email: normalUserDetails.email,
115 | 			password: normalUserDetails.password,
116 | 			fetchOptions: {
117 | 				onSuccess: (context) => {
118 | 					const header = context.response.headers.get("set-cookie");
119 | 					const cookies = parseSetCookieHeader(header || "");
120 | 					const signedCookie = cookies.get("better-auth.session_token")?.value;
121 | 					userHeaders.set(
122 | 						"cookie",
123 | 						`better-auth.session_token=${signedCookie}`,
124 | 					);
125 | 				},
126 | 			},
127 | 		});
128 | 		await authClient.organization.setActive({
129 | 			organizationId: org.data?.id,
130 | 			fetchOptions: {
131 | 				headers: userHeaders,
132 | 			},
133 | 		});
134 | 
135 | 		return { headers: userHeaders, user: normalUser, member };
136 | 	}
137 | 
138 | 	const org = await create(
139 | 		{
140 | 			name: "test",
141 | 			slug: "test",
142 | 			metadata: {
143 | 				test: "test",
144 | 			},
145 | 		},
146 | 		{
147 | 			onSuccess: sessionSetter(headers),
148 | 			headers,
149 | 		},
150 | 	);
151 | 	if (!org.data) throw new Error("Organization not created");
152 | 	const memberInfo = await auth.api.getActiveMember({ headers });
153 | 	if (!memberInfo) throw new Error("Member info not found");
154 | 
155 | 	// Create an admin user in the org.
156 | 	const {
157 | 		headers: adminHeaders,
158 | 		user: adminUser,
159 | 		member: adminMember,
160 | 	} = await createUser({
161 | 		role: "admin",
162 | 	});
163 | 
164 | 	// Create normal users in the org.
165 | 	const {
166 | 		headers: normalHeaders,
167 | 		user: normalUser,
168 | 		member: normalMember,
169 | 	} = await createUser({
170 | 		role: "member",
171 | 	});
172 | 
173 | 	/**
174 | 	 * The following test will:
175 | 	 * - Creation of a new role
176 | 	 * - Updating their own role to the newly created one (from owner to the new one)
177 | 	 * - Tests the `hasPermission` endpoint against the new role, for both a success and a failure case.
178 | 	 * - Additional fields passed in body, and correct return value & types.
179 | 	 */
180 | 	it("should successfully create a new role", async () => {
181 | 		// Create a new "test" role with permissions to create a project.
182 | 		const permission = {
183 | 			project: ["create"],
184 | 		};
185 | 		const testRole = await authClient.organization.createRole(
186 | 			{
187 | 				role: "test",
188 | 				permission,
189 | 				additionalFields: {
190 | 					color: "#000000",
191 | 				},
192 | 			},
193 | 			{
194 | 				headers,
195 | 			},
196 | 		);
197 | 		expect(testRole.error).toBeNull();
198 | 		expect(testRole.data?.success).toBe(true);
199 | 		expect(testRole.data?.roleData.permission).toEqual(permission);
200 | 		expect(testRole.data?.roleData.color).toBe("#000000");
201 | 		expect(testRole.data?.roleData.serverOnlyValue).toBe("server-only-value");
202 | 		expectTypeOf(testRole.data?.roleData.serverOnlyValue).toEqualTypeOf<
203 | 			string | undefined
204 | 		>();
205 | 		expectTypeOf(testRole.data?.roleData.role).toEqualTypeOf<
206 | 			string | undefined
207 | 		>();
208 | 		if (!testRole.data) return;
209 | 
210 | 		// Update the role to use the new one.
211 | 
212 | 		await auth.api.updateMemberRole({
213 | 			body: { memberId: normalMember.id, role: testRole.data.roleData.role },
214 | 			headers,
215 | 		});
216 | 
217 | 		// Test against `hasPermission` endpoint
218 | 		// Should fail because the user doesn't have the permission to delete a project.
219 | 		const shouldFail = await auth.api.hasPermission({
220 | 			body: {
221 | 				organizationId: org.data?.id,
222 | 				permissions: {
223 | 					project: ["delete"],
224 | 				},
225 | 			},
226 | 			headers: normalHeaders,
227 | 		});
228 | 		expect(shouldFail.success).toBe(false);
229 | 
230 | 		// Should pass because the user has the permission to create a project.
231 | 		const shouldPass = await auth.api.hasPermission({
232 | 			body: {
233 | 				organizationId: org.data?.id,
234 | 				permissions: {
235 | 					project: ["create"],
236 | 				},
237 | 			},
238 | 			headers: normalHeaders,
239 | 		});
240 | 		expect(shouldPass.success).toBe(true);
241 | 	});
242 | 
243 | 	it("should not be allowed to create a role without the right ac resource permissions", async () => {
244 | 		const testRole = await authClient.organization.createRole(
245 | 			{
246 | 				role: `test-${crypto.randomUUID()}`,
247 | 				permission: {
248 | 					project: ["create"],
249 | 				},
250 | 				additionalFields: {
251 | 					color: "#000000",
252 | 				},
253 | 			},
254 | 			{
255 | 				headers: normalHeaders,
256 | 			},
257 | 		);
258 | 		expect(testRole.data).toBeNull();
259 | 		if (!testRole.error) throw new Error("Test role error not found");
260 | 		expect(testRole.error.message).toEqual(
261 | 			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
262 | 		);
263 | 	});
264 | 
265 | 	it("should not be allowed to create a role with higher permissions than the current role", async () => {
266 | 		const testRole = await authClient.organization.createRole(
267 | 			{
268 | 				role: `test-${crypto.randomUUID()}`,
269 | 				permission: {
270 | 					sales: ["create", "delete", "create", "update", "read"], // Intentionally duplicate the "create" permission.
271 | 				},
272 | 				additionalFields: {
273 | 					color: "#000000",
274 | 				},
275 | 			},
276 | 			{
277 | 				headers: adminHeaders,
278 | 			},
279 | 		);
280 | 		expect(testRole.data).toBeNull();
281 | 		if (testRole.data) throw new Error("Test role created");
282 | 		expect(
283 | 			testRole.error.message?.startsWith(
284 | 				ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
285 | 			),
286 | 		).toBe(true);
287 | 		expect("missingPermissions" in testRole.error).toBe(true);
288 | 		if (!("missingPermissions" in testRole.error)) return;
289 | 		expect(testRole.error.missingPermissions).toEqual([
290 | 			"sales:delete",
291 | 			"sales:update",
292 | 		]);
293 | 	});
294 | 
295 | 	it("should not be allowed to create a role which is either predefined or already exists in DB", async () => {
296 | 		const testRole = await authClient.organization.createRole(
297 | 			{
298 | 				role: "admin", // This is a predefined role.
299 | 				permission: {
300 | 					project: ["create"],
301 | 				},
302 | 				additionalFields: {
303 | 					color: "#000000",
304 | 				},
305 | 			},
306 | 			{
307 | 				headers,
308 | 			},
309 | 		);
310 | 		expect(testRole.data).toBeNull();
311 | 		if (!testRole.error) throw new Error("Test role error not found");
312 | 		expect(testRole.error.message).toEqual(
313 | 			ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
314 | 		);
315 | 
316 | 		const testRole2 = await authClient.organization.createRole(
317 | 			{
318 | 				role: "test", // This is a role that was created in the previous test.
319 | 				permission: {
320 | 					project: ["create"],
321 | 				},
322 | 				additionalFields: {
323 | 					color: "#000000",
324 | 				},
325 | 			},
326 | 			{
327 | 				headers,
328 | 			},
329 | 		);
330 | 		expect(testRole2.data).toBeNull();
331 | 		if (!testRole2.error) throw new Error("Test role error not found");
332 | 		expect(testRole2.error.message).toEqual(
333 | 			ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
334 | 		);
335 | 	});
336 | 
337 | 	it("should delete a role by id", async () => {
338 | 		const testRole = await authClient.organization.createRole(
339 | 			{
340 | 				role: `test-${crypto.randomUUID()}`,
341 | 				permission: {
342 | 					project: ["create"],
343 | 				},
344 | 				additionalFields: {
345 | 					color: "#000000",
346 | 				},
347 | 			},
348 | 			{
349 | 				headers,
350 | 			},
351 | 		);
352 | 		if (!testRole.data) throw testRole.error;
353 | 		const roleId = testRole.data.roleData.id;
354 | 
355 | 		const res = await auth.api.deleteOrgRole({
356 | 			body: { roleId },
357 | 			headers,
358 | 		});
359 | 		expect(res).not.toBeNull();
360 | 	});
361 | 
362 | 	it("should delete a role by name", async () => {
363 | 		const testRole = await authClient.organization.createRole(
364 | 			{
365 | 				role: `test-${crypto.randomUUID()}`,
366 | 				permission: {
367 | 					project: ["create"],
368 | 				},
369 | 				additionalFields: {
370 | 					color: "#000000",
371 | 				},
372 | 			},
373 | 			{
374 | 				headers,
375 | 			},
376 | 		);
377 | 		if (!testRole.data) throw testRole.error;
378 | 		const roleName = testRole.data.roleData.role;
379 | 
380 | 		const res = await auth.api.deleteOrgRole({
381 | 			body: { roleName },
382 | 			headers,
383 | 		});
384 | 		expect(res).not.toBeNull();
385 | 	});
386 | 
387 | 	it("should not be allowed to delete a role without nessesary permissions", async () => {
388 | 		const testRole = await authClient.organization.createRole(
389 | 			{
390 | 				role: `test-${crypto.randomUUID()}`,
391 | 				permission: {
392 | 					project: ["create"],
393 | 				},
394 | 				additionalFields: {
395 | 					color: "#000000",
396 | 				},
397 | 			},
398 | 			{
399 | 				headers: adminHeaders,
400 | 			},
401 | 		);
402 | 		if (!testRole.data) throw testRole.error;
403 | 		expect(
404 | 			auth.api.deleteOrgRole({
405 | 				body: { roleName: testRole.data.roleData.role },
406 | 				headers: normalHeaders,
407 | 			}),
408 | 		).rejects.toThrow(
409 | 			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE,
410 | 		);
411 | 	});
412 | 
413 | 	it("should not be allowed to delete a role that doesn't exist", async () => {
414 | 		try {
415 | 			const res = await auth.api.deleteOrgRole({
416 | 				body: { roleName: "non-existent-role" },
417 | 				headers,
418 | 			});
419 | 			expect(res).toBeNull();
420 | 		} catch (error: any) {
421 | 			if ("body" in error && "message" in error.body) {
422 | 				expect(error.body.message).toBe(
423 | 					ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
424 | 				);
425 | 			} else {
426 | 				throw error;
427 | 			}
428 | 		}
429 | 	});
430 | 
431 | 	it("should list roles", async () => {
432 | 		const permission = {
433 | 			project: ["create"],
434 | 			ac: ["read", "update", "create", "delete"],
435 | 		};
436 | 		await authClient.organization.createRole(
437 | 			{
438 | 				role: `list-test-role`,
439 | 				permission,
440 | 				additionalFields: {
441 | 					color: "#123",
442 | 				},
443 | 			},
444 | 			{
445 | 				headers,
446 | 			},
447 | 		);
448 | 
449 | 		const res = await auth.api.listOrgRoles({ headers });
450 | 		expect(res).not.toBeNull();
451 | 		expect(res.length).toBeGreaterThan(0);
452 | 		expect(typeof res[0]!.permission === "string").toBe(false);
453 | 		const foundRole = res.find((x) => x.role === "list-test-role");
454 | 		expect(foundRole).not.toBeNull();
455 | 		expect(foundRole?.permission).toEqual(permission);
456 | 		expect(foundRole?.color).toBe(`#123`);
457 | 		expectTypeOf(foundRole?.color).toEqualTypeOf<string | undefined>();
458 | 		expectTypeOf(foundRole?.serverOnlyValue).toEqualTypeOf<
459 | 			string | undefined
460 | 		>();
461 | 	});
462 | 
463 | 	it("should not be allowed to list roles without nessesary permissions", async () => {
464 | 		expect(auth.api.listOrgRoles({ headers: normalHeaders })).rejects.toThrow(
465 | 			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE,
466 | 		);
467 | 	});
468 | 
469 | 	it("should get a role by id", async () => {
470 | 		const testRole = await authClient.organization.createRole(
471 | 			{
472 | 				role: `read-test-role-${crypto.randomUUID()}`,
473 | 				permission: {
474 | 					project: ["create"],
475 | 				},
476 | 				additionalFields: {
477 | 					color: "#000000",
478 | 				},
479 | 			},
480 | 			{
481 | 				headers,
482 | 			},
483 | 		);
484 | 		if (!testRole.data) throw testRole.error;
485 | 		const roleId = testRole.data.roleData.id;
486 | 		const res = await auth.api.getOrgRole({
487 | 			query: {
488 | 				roleId,
489 | 				organizationId: org.data?.id,
490 | 			},
491 | 			headers,
492 | 		});
493 | 		expect(res).not.toBeNull();
494 | 		expect(res.role).toBe(testRole.data.roleData.role);
495 | 		expect(res.permission).toEqual(testRole.data.roleData.permission);
496 | 		expect(res.color).toBe("#000000");
497 | 		expectTypeOf(res.color).toEqualTypeOf<string>();
498 | 	});
499 | 
500 | 	it("should get a role by name", async () => {
501 | 		const testRole = await authClient.organization.createRole(
502 | 			{
503 | 				role: `read-test-role-${crypto.randomUUID()}`,
504 | 				permission: {
505 | 					project: ["create"],
506 | 				},
507 | 				additionalFields: {
508 | 					color: "#000000",
509 | 				},
510 | 			},
511 | 			{
512 | 				headers,
513 | 			},
514 | 		);
515 | 		if (!testRole.data) throw testRole.error;
516 | 		const roleName = testRole.data.roleData.role;
517 | 
518 | 		const res = await auth.api.getOrgRole({
519 | 			query: {
520 | 				roleName,
521 | 				organizationId: org.data?.id,
522 | 			},
523 | 			headers,
524 | 		});
525 | 		expect(res).not.toBeNull();
526 | 		expect(res.role).toBe(testRole.data.roleData.role);
527 | 		expect(res.permission).toEqual(testRole.data.roleData.permission);
528 | 		expect(res.color).toBe("#000000");
529 | 		expectTypeOf(res.color).toEqualTypeOf<string>();
530 | 	});
531 | 
532 | 	it("should update a role's permission by id", async () => {
533 | 		const testRole = await authClient.organization.createRole(
534 | 			{
535 | 				role: `update-test-role-${crypto.randomUUID()}`,
536 | 				permission: {
537 | 					project: ["create"],
538 | 				},
539 | 				additionalFields: {
540 | 					color: "#000000",
541 | 				},
542 | 			},
543 | 			{
544 | 				headers,
545 | 			},
546 | 		);
547 | 		if (!testRole.data) throw testRole.error;
548 | 		const roleId = testRole.data.roleData.id;
549 | 		const res = await auth.api.updateOrgRole({
550 | 			body: {
551 | 				roleId,
552 | 				data: { permission: { project: ["create", "delete"] } },
553 | 			},
554 | 			headers,
555 | 		});
556 | 		expect(res).not.toBeNull();
557 | 		expect(res.roleData.role).toBe(testRole.data.roleData.role);
558 | 		expect(res.roleData.permission).toEqual({ project: ["create", "delete"] });
559 | 	});
560 | 
561 | 	it("should update a role's name by name", async () => {
562 | 		const testRole = await authClient.organization.createRole(
563 | 			{
564 | 				role: `test-${crypto.randomUUID()}`,
565 | 				permission: {
566 | 					project: ["create"],
567 | 				},
568 | 				additionalFields: {
569 | 					color: "#000000",
570 | 				},
571 | 			},
572 | 			{ headers },
573 | 		);
574 | 		if (!testRole.data) throw testRole.error;
575 | 		const roleName = testRole.data.roleData.role;
576 | 
577 | 		const res = await auth.api.updateOrgRole({
578 | 			body: { roleName, data: { roleName: `updated-${roleName}` } },
579 | 			headers,
580 | 		});
581 | 		expect(res).not.toBeNull();
582 | 		expect(res.roleData.role).toBe(`updated-${roleName}`);
583 | 
584 | 		const res2 = await auth.api.getOrgRole({
585 | 			query: {
586 | 				roleName: `updated-${roleName}`,
587 | 				organizationId: org.data?.id,
588 | 			},
589 | 			headers,
590 | 		});
591 | 		expect(res2).not.toBeNull();
592 | 		expect(res2.role).toBe(`updated-${roleName}`);
593 | 	});
594 | 
595 | 	it("should not be allowed to update a role without the right ac resource permissions", async () => {
596 | 		const testRole = await authClient.organization.createRole(
597 | 			{
598 | 				role: `update-not-allowed-${crypto.randomUUID()}`,
599 | 				permission: {
600 | 					project: ["create"],
601 | 				},
602 | 				additionalFields: {
603 | 					color: "#000000",
604 | 				},
605 | 			},
606 | 			{ headers },
607 | 		);
608 | 		if (!testRole.data) throw testRole.error;
609 | 		const roleId = testRole.data.roleData.id;
610 | 		await expect(
611 | 			auth.api.updateOrgRole({
612 | 				body: {
613 | 					roleId,
614 | 					data: { roleName: `updated-${testRole.data.roleData.role}` },
615 | 				},
616 | 				headers: normalHeaders,
617 | 			}),
618 | 		).rejects.toThrow();
619 | 	});
620 | 
621 | 	it("should be able to update additional fields", async () => {
622 | 		const testRole = await authClient.organization.createRole(
623 | 			{
624 | 				role: `test-${crypto.randomUUID()}`,
625 | 				permission: {
626 | 					project: ["create"],
627 | 				},
628 | 				additionalFields: {
629 | 					color: "#000000",
630 | 					//@ts-expect-error - intentionally invalid key
631 | 					someInvalidKey: "this would be ignored by zod",
632 | 				},
633 | 			},
634 | 			{
635 | 				headers,
636 | 			},
637 | 		);
638 | 		if (!testRole.data) throw testRole.error;
639 | 		const roleId = testRole.data.roleData.id;
640 | 		const res = await auth.api.updateOrgRole({
641 | 			body: { roleId, data: { color: "#111111" } },
642 | 			headers,
643 | 		});
644 | 		expect(res).not.toBeNull();
645 | 		expect(res.roleData.color).toBe("#111111");
646 | 		//@ts-expect-error - intentionally invalid key
647 | 		expect(res.roleData.someInvalidKey).toBeUndefined();
648 | 	});
649 | 
650 | 	/**
651 | 	 * Security test cases for the privilege escalation vulnerability fix
652 | 	 * These tests verify that member queries properly filter by userId to prevent
653 | 	 * unauthorized privilege escalation where any member could gain admin permissions
654 | 	 */
655 | 	it("should not allow member to list roles using another member's permissions", async () => {
656 | 		// Create a fresh member for this test to avoid role contamination
657 | 		const {
658 | 			headers: freshMemberHeaders,
659 | 			user: freshMemberUser,
660 | 			member: freshMember,
661 | 		} = await createUser({
662 | 			role: "member",
663 | 		});
664 | 
665 | 		// Create a test role that only admin can read
666 | 		const adminOnlyRole = await authClient.organization.createRole(
667 | 			{
668 | 				role: `admin-only-${crypto.randomUUID()}`,
669 | 				permission: {
670 | 					project: ["delete"],
671 | 				},
672 | 				additionalFields: {
673 | 					color: "#ff0000",
674 | 				},
675 | 			},
676 | 			{
677 | 				headers,
678 | 			},
679 | 		);
680 | 		if (!adminOnlyRole.data) throw adminOnlyRole.error;
681 | 
682 | 		// Try to list roles as a regular member - should succeed but with member permissions
683 | 		const listAsMembers = await auth.api.listOrgRoles({
684 | 			query: { organizationId: org.data?.id },
685 | 			headers: freshMemberHeaders,
686 | 		});
687 | 
688 | 		// Member should be able to list roles (they have ac:read permission)
689 | 		expect(listAsMembers).toBeDefined();
690 | 		expect(Array.isArray(listAsMembers)).toBe(true);
691 | 	});
692 | 
693 | 	it("should not allow member to get role details using another member's permissions", async () => {
694 | 		// Create a fresh member for this test to avoid role contamination
695 | 		const {
696 | 			headers: freshMemberHeaders,
697 | 			user: freshMemberUser,
698 | 			member: freshMember,
699 | 		} = await createUser({
700 | 			role: "member",
701 | 		});
702 | 
703 | 		// Create a test role
704 | 		const testRole = await authClient.organization.createRole(
705 | 			{
706 | 				role: `test-get-role-${crypto.randomUUID()}`,
707 | 				permission: {
708 | 					project: ["read"],
709 | 				},
710 | 				additionalFields: {
711 | 					color: "#ff0000",
712 | 				},
713 | 			},
714 | 			{
715 | 				headers,
716 | 			},
717 | 		);
718 | 		if (!testRole.data) throw testRole.error;
719 | 
720 | 		// Try to get role as a regular member - should succeed with member permissions
721 | 		const getRoleAsMember = await auth.api.getOrgRole({
722 | 			query: {
723 | 				organizationId: org.data?.id,
724 | 				roleId: testRole.data.roleData.id,
725 | 			},
726 | 			headers: freshMemberHeaders,
727 | 		});
728 | 
729 | 		// Member should be able to read the role (they have ac:read permission)
730 | 		expect(getRoleAsMember).toBeDefined();
731 | 		expect(getRoleAsMember.id).toBe(testRole.data.roleData.id);
732 | 	});
733 | 
734 | 	it("should not allow member to update roles without proper permissions (privilege escalation test)", async () => {
735 | 		// Create a fresh member for this test to avoid role contamination
736 | 		const {
737 | 			headers: freshMemberHeaders,
738 | 			user: freshMemberUser,
739 | 			member: freshMember,
740 | 		} = await createUser({
741 | 			role: "member",
742 | 		});
743 | 
744 | 		// Create a test role that the owner will create
745 | 		const vulnerableRole = await authClient.organization.createRole(
746 | 			{
747 | 				role: `vulnerable-role-${crypto.randomUUID()}`,
748 | 				permission: {
749 | 					project: ["read"],
750 | 				},
751 | 				additionalFields: {
752 | 					color: "#ff0000",
753 | 				},
754 | 			},
755 | 			{
756 | 				headers, // owner headers
757 | 			},
758 | 		);
759 | 		if (!vulnerableRole.data) throw vulnerableRole.error;
760 | 
761 | 		// Regular member should NOT be able to update the role
762 | 		// This tests the privilege escalation vulnerability fix
763 | 		await expect(
764 | 			auth.api.updateOrgRole({
765 | 				body: {
766 | 					roleId: vulnerableRole.data.roleData.id,
767 | 					data: {
768 | 						permission: {
769 | 							ac: ["create", "update", "delete"], // Try to escalate privileges
770 | 							organization: ["update", "delete"],
771 | 							project: ["create", "read", "update", "delete"],
772 | 						},
773 | 					},
774 | 				},
775 | 				headers: freshMemberHeaders, // member headers
776 | 			}),
777 | 		).rejects.toThrow();
778 | 
779 | 		// Verify the role permissions haven't changed
780 | 		const roleCheck = await auth.api.getOrgRole({
781 | 			query: {
782 | 				organizationId: org.data?.id,
783 | 				roleId: vulnerableRole.data.roleData.id,
784 | 			},
785 | 			headers,
786 | 		});
787 | 		expect(roleCheck.permission).toEqual({
788 | 			project: ["read"],
789 | 		});
790 | 	});
791 | 
792 | 	it("should properly identify the correct member when checking permissions", async () => {
793 | 		// Create a fresh member for this test to avoid role contamination
794 | 		const {
795 | 			headers: freshMemberHeaders,
796 | 			user: freshMemberUser,
797 | 			member: freshMember,
798 | 		} = await createUser({
799 | 			role: "member",
800 | 		});
801 | 
802 | 		// This test ensures that the member lookup uses both organizationId AND userId
803 | 		// Create a role that only owner can update
804 | 		const ownerOnlyRole = await authClient.organization.createRole(
805 | 			{
806 | 				role: `owner-only-update-${crypto.randomUUID()}`,
807 | 				permission: {
808 | 					sales: ["delete"],
809 | 				},
810 | 				additionalFields: {
811 | 					color: "#ff0000",
812 | 				},
813 | 			},
814 | 			{
815 | 				headers, // owner headers
816 | 			},
817 | 		);
818 | 		if (!ownerOnlyRole.data) throw ownerOnlyRole.error;
819 | 
820 | 		// Member should not be able to update (doesn't have ac:update)
821 | 		await expect(
822 | 			auth.api.updateOrgRole({
823 | 				body: {
824 | 					roleId: ownerOnlyRole.data.roleData.id,
825 | 					data: {
826 | 						roleName: "hijacked-role",
827 | 					},
828 | 				},
829 | 				headers: freshMemberHeaders,
830 | 			}),
831 | 		).rejects.toThrow(
832 | 			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE,
833 | 		);
834 | 
835 | 		// Admin should be able to update (has ac:update)
836 | 		const adminUpdate = await auth.api.updateOrgRole({
837 | 			body: {
838 | 				roleId: ownerOnlyRole.data.roleData.id,
839 | 				data: {
840 | 					roleName: `admin-updated-${ownerOnlyRole.data.roleData.role}`,
841 | 				},
842 | 			},
843 | 			headers: adminHeaders,
844 | 		});
845 | 		expect(adminUpdate).toBeDefined();
846 | 		expect(adminUpdate.roleData.role).toContain("admin-updated");
847 | 	});
848 | 
849 | 	it("should not allow cross-organization privilege escalation", async () => {
850 | 		// Create a fresh member for this test to avoid role contamination
851 | 		const {
852 | 			headers: freshMemberHeaders,
853 | 			user: freshMemberUser,
854 | 			member: freshMember,
855 | 		} = await createUser({
856 | 			role: "member",
857 | 		});
858 | 
859 | 		// Create a second organization
860 | 		const org2 = await authClient.organization.create(
861 | 			{
862 | 				name: "second-org",
863 | 				slug: `second-org-${crypto.randomUUID()}`,
864 | 			},
865 | 			{
866 | 				onSuccess: sessionSetter(headers),
867 | 				headers,
868 | 			},
869 | 		);
870 | 		if (!org2.data) throw new Error("Second organization not created");
871 | 
872 | 		// Try to list roles from org1 while active in org2 - should fail
873 | 		await authClient.organization.setActive({
874 | 			organizationId: org2.data.id,
875 | 			fetchOptions: {
876 | 				headers: freshMemberHeaders,
877 | 			},
878 | 		});
879 | 
880 | 		// This should fail because the member is not in org2
881 | 		await expect(
882 | 			auth.api.listOrgRoles({
883 | 				query: { organizationId: org2.data.id },
884 | 				headers: freshMemberHeaders,
885 | 			}),
886 | 		).rejects.toThrow("You are not a member of this organization");
887 | 
888 | 		// Switch back to org1
889 | 		await authClient.organization.setActive({
890 | 			organizationId: org.data?.id,
891 | 			fetchOptions: {
892 | 				headers: freshMemberHeaders,
893 | 			},
894 | 		});
895 | 	});
896 | });
897 | 
```

--------------------------------------------------------------------------------
/docs/components/builder/index.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Moon, PlusIcon, Sun } from "lucide-react";
  2 | import {
  3 | 	Dialog,
  4 | 	DialogContent,
  5 | 	DialogDescription,
  6 | 	DialogHeader,
  7 | 	DialogTitle,
  8 | 	DialogTrigger,
  9 | } from "../ui/dialog";
 10 | import {
 11 | 	Card,
 12 | 	CardContent,
 13 | 	CardFooter,
 14 | 	CardHeader,
 15 | 	CardTitle,
 16 | } from "../ui/card";
 17 | import SignIn from "./sign-in";
 18 | import { SignUp } from "./sign-up";
 19 | import { AuthTabs } from "./tabs";
 20 | import { Label } from "../ui/label";
 21 | import { Switch } from "../ui/switch";
 22 | import { Separator } from "../ui/separator";
 23 | import { useState } from "react";
 24 | import CodeTabs from "./code-tabs";
 25 | import { cn } from "@/lib/utils";
 26 | import { socialProviders } from "./social-provider";
 27 | import { useAtom } from "jotai";
 28 | import { optionsAtom } from "./store";
 29 | import { useTheme } from "next-themes";
 30 | import { ScrollArea } from "../ui/scroll-area";
 31 | const frameworks = [
 32 | 	{
 33 | 		title: "Next.js",
 34 | 		description: "The React Framework for Production",
 35 | 		Icon: () => (
 36 | 			<svg
 37 | 				xmlns="http://www.w3.org/2000/svg"
 38 | 				width="2em"
 39 | 				height="2em"
 40 | 				viewBox="0 0 15 15"
 41 | 			>
 42 | 				<path
 43 | 					fill="currentColor"
 44 | 					fillRule="evenodd"
 45 | 					d="M0 7.5a7.5 7.5 0 1 1 11.698 6.216L4.906 4.21A.5.5 0 0 0 4 4.5V12h1V6.06l5.83 8.162A7.5 7.5 0 0 1 0 7.5M10 10V4h1v6z"
 46 | 					clipRule="evenodd"
 47 | 				></path>
 48 | 			</svg>
 49 | 		),
 50 | 	},
 51 | 	{
 52 | 		title: "Nuxt",
 53 | 		description: "The Intuitive Vue Framework",
 54 | 		Icon: () => (
 55 | 			<svg
 56 | 				xmlns="http://www.w3.org/2000/svg"
 57 | 				width="2em"
 58 | 				height="2em"
 59 | 				viewBox="0 0 256 256"
 60 | 			>
 61 | 				<g fill="none">
 62 | 					<rect width="256" height="256" fill="#242938" rx="60"></rect>
 63 | 					<path
 64 | 						fill="#00DC82"
 65 | 						d="M138.787 189.333h68.772c2.184.001 4.33-.569 6.222-1.652a12.4 12.4 0 0 0 4.554-4.515a12.24 12.24 0 0 0-.006-12.332l-46.185-79.286a12.4 12.4 0 0 0-4.553-4.514a12.53 12.53 0 0 0-12.442 0a12.4 12.4 0 0 0-4.553 4.514l-11.809 20.287l-23.09-39.67a12.4 12.4 0 0 0-4.555-4.513a12.54 12.54 0 0 0-12.444 0a12.4 12.4 0 0 0-4.555 4.513L36.67 170.834a12.24 12.24 0 0 0-.005 12.332a12.4 12.4 0 0 0 4.554 4.515a12.5 12.5 0 0 0 6.222 1.652h43.17c17.104 0 29.718-7.446 38.397-21.973l21.072-36.169l11.287-19.356l33.873 58.142h-45.16zm-48.88-19.376l-30.127-.007l45.16-77.518l22.533 38.759l-15.087 25.906c-5.764 9.426-12.312 12.86-22.48 12.86"
 66 | 					></path>
 67 | 				</g>
 68 | 			</svg>
 69 | 		),
 70 | 	},
 71 | 	{
 72 | 		title: "SvelteKit",
 73 | 		description: "Web development for the rest of us",
 74 | 		Icon: () => (
 75 | 			<svg
 76 | 				xmlns="http://www.w3.org/2000/svg"
 77 | 				width="2em"
 78 | 				height="2em"
 79 | 				viewBox="0 0 256 256"
 80 | 			>
 81 | 				<g fill="none">
 82 | 					<rect width="256" height="256" fill="#FF3E00" rx="60"></rect>
 83 | 					<g clipPath="url(#skillIconsSvelte0)">
 84 | 						<path
 85 | 							fill="#fff"
 86 | 							d="M193.034 61.797c-16.627-23.95-49.729-30.966-73.525-15.865L77.559 72.78c-11.44 7.17-19.372 18.915-21.66 32.186c-1.984 11.136-.306 22.576 5.033 32.492c-3.66 5.491-6.102 11.593-7.17 18c-2.44 13.576.764 27.61 8.696 38.745c16.78 23.95 49.728 30.966 73.525 15.865l41.949-26.695c11.441-7.17 19.373-18.915 21.661-32.187c1.983-11.135.305-22.576-5.034-32.491c3.661-5.492 6.102-11.593 7.17-18c2.593-13.729-.61-27.763-8.695-38.898"
 87 | 						></path>
 88 | 						<path
 89 | 							fill="#FF3E00"
 90 | 							d="M115.39 196.491a33.25 33.25 0 0 1-35.695-13.271c-4.881-6.712-6.712-15.101-5.34-23.339c.306-1.373.611-2.593.916-3.966l.763-2.44L78.169 155a55.6 55.6 0 0 0 16.475 8.237l1.525.458l-.152 1.525c-.153 2.136.458 4.424 1.678 6.255c2.441 3.508 6.712 5.186 10.83 4.118c.916-.305 1.831-.61 2.594-1.068l41.796-26.695c2.136-1.372 3.509-3.355 3.966-5.796s-.152-5.034-1.525-7.017c-2.441-3.509-6.712-5.034-10.831-3.966c-.915.305-1.83.61-2.593 1.068l-16.017 10.22c-2.593 1.678-5.491 2.898-8.542 3.661a33.25 33.25 0 0 1-35.695-13.271c-4.729-6.712-6.712-15.102-5.186-23.339c1.372-7.932 6.254-15.102 13.118-19.373l41.949-26.695c2.593-1.678 5.492-2.898 8.543-3.814a33.25 33.25 0 0 1 35.695 13.272c4.881 6.712 6.711 15.101 5.339 23.339c-.306 1.373-.611 2.593-1.068 3.966l-.763 2.44l-2.136-1.525a55.6 55.6 0 0 0-16.474-8.237l-1.526-.458l.153-1.525c.153-2.136-.458-4.424-1.678-6.255c-2.441-3.508-6.712-5.034-10.83-3.966c-.916.305-1.831.61-2.594 1.068l-41.796 26.695c-2.136 1.373-3.509 3.356-3.966 5.797s.152 5.034 1.525 7.017c2.441 3.508 6.712 5.033 10.831 3.966c.915-.305 1.83-.611 2.593-1.068l16.017-10.22c2.593-1.678 5.491-2.899 8.542-3.814a33.25 33.25 0 0 1 35.695 13.271c4.881 6.712 6.712 15.102 5.339 23.339c-1.373 7.932-6.254 15.102-13.119 19.373l-41.949 26.695c-2.593 1.678-5.491 2.898-8.542 3.813"
 91 | 						></path>
 92 | 					</g>
 93 | 					<defs>
 94 | 						<clipPath id="skillIconsSvelte0">
 95 | 							<path fill="#fff" d="M53 38h149.644v180H53z"></path>
 96 | 						</clipPath>
 97 | 					</defs>
 98 | 				</g>
 99 | 			</svg>
100 | 		),
101 | 	},
102 | 	{
103 | 		title: "SolidStart",
104 | 		description: "Fine-grained reactivity goes fullstack",
105 | 		Icon: () => (
106 | 			<svg
107 | 				data-hk="00000010210"
108 | 				width="2em"
109 | 				height="2em"
110 | 				viewBox="0 0 500 500"
111 | 				fill="none"
112 | 				xmlns="http://www.w3.org/2000/svg"
113 | 				role="presentation"
114 | 			>
115 | 				<path
116 | 					d="M233.205 430.856L304.742 425.279C304.742 425.279 329.208 421.295 343.569 397.659L293.041 385.443L233.205 430.856Z"
117 | 					fill="url(#paint0_linear_1_2)"
118 | 				></path>
119 | 				<path
120 | 					d="M134.278 263.278C113.003 264.341 73.6443 268.059 73.6443 268.059L245.173 392.614L284.265 402.44L343.569 397.925L170.977 273.105C170.977 273.105 157.148 263.278 137.203 263.278C136.139 263.278 135.342 263.278 134.278 263.278Z"
121 | 					fill="url(#paint1_linear_1_2)"
122 | 				></path>
123 | 				<path
124 | 					d="M355.536 238.58L429.2 234.065C429.2 234.065 454.464 230.348 468.825 206.977L416.435 193.964L355.536 238.58Z"
125 | 					fill="url(#paint2_linear_1_2)"
126 | 				></path>
127 | 				<path
128 | 					d="M251.289 68.6128C229.217 69.4095 188.795 72.5964 188.795 72.5964L367.503 200.072L407.926 210.429L469.09 206.712L289.318 78.9702C289.318 78.9702 274.426 68.6128 253.417 68.6128C252.885 68.6128 252.087 68.6128 251.289 68.6128Z"
129 | 					fill="url(#paint3_linear_1_2)"
130 | 				></path>
131 | 				<path
132 | 					d="M31.0946 295.679C30.8287 295.945 30.8287 296.21 30.8287 296.475L77.8993 330.469L202.623 420.764C228.95 439.62 264.586 431.653 282.67 402.44L187.465 333.921L110.077 277.62C100.504 270.715 89.8663 267.528 79.2289 267.528C60.6134 267.528 42.2639 277.354 31.0946 295.679Z"
133 | 					fill="url(#paint4_linear_1_2)"
134 | 				></path>
135 | 				<path
136 | 					d="M147.043 99.9505C147.043 100.216 146.776 100.482 146.511 100.747L195.442 135.538L244.374 170.062L325.751 227.957C353.142 247.345 389.841 239.642 407.925 210.695L358.461 175.374L308.997 140.318L228.153 82.6881C218.047 75.5177 206.611 72.0652 195.442 72.0652C176.561 72.3308 158.212 81.8915 147.043 99.9505Z"
137 | 					fill="url(#paint5_linear_1_2)"
138 | 				></path>
139 | 				<path
140 | 					d="M112.471 139.255L175.497 208.305C178.423 212.289 181.614 216.006 185.337 219.193L308.199 354.105L369.364 350.387C387.448 321.439 380.002 282.135 352.611 262.748L271.234 204.852L222.568 170.328L173.636 135.538L112.471 139.255Z"
141 | 					fill="url(#paint6_linear_1_2)"
142 | 				></path>
143 | 				<path
144 | 					d="M111.939 140.052C94.1213 168.734 101.567 207.509 128.427 226.629L209.005 283.994L258.735 319.049L308.199 354.105C326.283 325.158 318.836 285.852 291.445 266.465L112.471 139.255C112.471 139.521 112.204 139.787 111.939 140.052Z"
145 | 					fill="url(#paint7_linear_1_2)"
146 | 				></path>
147 | 				<defs>
148 | 					<linearGradient
149 | 						id="paint0_linear_1_2"
150 | 						x1="359.728"
151 | 						y1="56.8062"
152 | 						x2="265.623"
153 | 						y2="521.28"
154 | 						gradientUnits="userSpaceOnUse"
155 | 					>
156 | 						<stop stopColor="#1593F5"></stop>
157 | 						<stop offset="1" stopColor="#0084CE"></stop>
158 | 					</linearGradient>
159 | 					<linearGradient
160 | 						id="paint1_linear_1_2"
161 | 						x1="350.496"
162 | 						y1="559.872"
163 | 						x2="-44.0802"
164 | 						y2="-73.2062"
165 | 						gradientUnits="userSpaceOnUse"
166 | 					>
167 | 						<stop stopColor="#1593F5"></stop>
168 | 						<stop offset="1" stopColor="#0084CE"></stop>
169 | 					</linearGradient>
170 | 					<linearGradient
171 | 						id="paint2_linear_1_2"
172 | 						x1="610.25"
173 | 						y1="570.526"
174 | 						x2="372.635"
175 | 						y2="144.034"
176 | 						gradientUnits="userSpaceOnUse"
177 | 					>
178 | 						<stop stopColor="white"></stop>
179 | 						<stop offset="1" stopColor="#15ABFF"></stop>
180 | 					</linearGradient>
181 | 					<linearGradient
182 | 						id="paint3_linear_1_2"
183 | 						x1="188.808"
184 | 						y1="-180.608"
185 | 						x2="390.515"
186 | 						y2="281.703"
187 | 						gradientUnits="userSpaceOnUse"
188 | 					>
189 | 						<stop stopColor="white"></stop>
190 | 						<stop offset="1" stopColor="#79CFFF"></stop>
191 | 					</linearGradient>
192 | 					<linearGradient
193 | 						id="paint4_linear_1_2"
194 | 						x1="415.84"
195 | 						y1="-4.74684"
196 | 						x2="95.1922"
197 | 						y2="439.83"
198 | 						gradientUnits="userSpaceOnUse"
199 | 					>
200 | 						<stop stopColor="#0057E5"></stop>
201 | 						<stop offset="1" stopColor="#0084CE"></stop>
202 | 					</linearGradient>
203 | 					<linearGradient
204 | 						id="paint5_linear_1_2"
205 | 						x1="343.141"
206 | 						y1="-21.5427"
207 | 						x2="242.301"
208 | 						y2="256.708"
209 | 						gradientUnits="userSpaceOnUse"
210 | 					>
211 | 						<stop stopColor="white"></stop>
212 | 						<stop offset="1" stopColor="#15ABFF"></stop>
213 | 					</linearGradient>
214 | 					<linearGradient
215 | 						id="paint6_linear_1_2"
216 | 						x1="469.095"
217 | 						y1="533.421"
218 | 						x2="-37.6939"
219 | 						y2="-135.731"
220 | 						gradientUnits="userSpaceOnUse"
221 | 					>
222 | 						<stop stopColor="white"></stop>
223 | 						<stop offset="1" stopColor="#79CFFF"></stop>
224 | 					</linearGradient>
225 | 					<linearGradient
226 | 						id="paint7_linear_1_2"
227 | 						x1="380.676"
228 | 						y1="-89.0869"
229 | 						x2="120.669"
230 | 						y2="424.902"
231 | 						gradientUnits="userSpaceOnUse"
232 | 					>
233 | 						<stop stopColor="white"></stop>
234 | 						<stop offset="1" stopColor="#79CFFF"></stop>
235 | 					</linearGradient>
236 | 				</defs>
237 | 			</svg>
238 | 		),
239 | 	},
240 | ];
241 | 
242 | export function Builder() {
243 | 	const [currentStep, setCurrentStep] = useState(0);
244 | 
245 | 	const [options, setOptions] = useAtom(optionsAtom);
246 | 	const { setTheme, resolvedTheme } = useTheme();
247 | 	return (
248 | 		<Dialog>
249 | 			<DialogTrigger asChild>
250 | 				<button className="bg-stone-950 no-underline group cursor-pointer relative  p-px text-xs font-semibold leading-6  text-white md:inline-block hidden">
251 | 					<span className="absolute inset-0 overflow-hidden rounded-sm">
252 | 						<span className="absolute inset-0 rounded-sm bg-[image:radial-gradient(75%_100%_at_50%_0%,rgba(56,189,248,0.6)_0%,rgba(56,189,248,0)_75%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100"></span>
253 | 					</span>
254 | 					<div className="relative flex space-x-2 items-center z-10 rounded-none bg-zinc-950 py-2 px-4 ring-1 ring-white/10 ">
255 | 						<PlusIcon size={14} />
256 | 						<span>Create Sign in Box</span>
257 | 					</div>
258 | 					<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-stone-800/90 to-emerald-400/0 transition-opacity duration-500 group-hover:opacity-40"></span>
259 | 				</button>
260 | 			</DialogTrigger>
261 | 			<DialogContent className="max-w-7xl h-5/6 overflow-clip !rounded-none">
262 | 				<DialogHeader>
263 | 					<DialogTitle>Create Sign in Box</DialogTitle>
264 | 					<DialogDescription>
265 | 						Configure the sign in box to your liking and copy the code to your
266 | 						application.
267 | 					</DialogDescription>
268 | 				</DialogHeader>
269 | 
270 | 				<div className="flex gap-4 md:gap-12 flex-col md:flex-row items-center md:items-start">
271 | 					<div className={cn("w-4/12")}>
272 | 						<div
273 | 							className="overflow-scroll h-[580px] relative"
274 | 							style={{
275 | 								scrollbarWidth: "none",
276 | 								scrollbarColor: "transparent transparent",
277 | 								//@ts-expect-error
278 | 								"&::-webkit-scrollbar": {
279 | 									display: "none",
280 | 								},
281 | 							}}
282 | 						>
283 | 							{options.signUp ? (
284 | 								<AuthTabs
285 | 									tabs={[
286 | 										{
287 | 											title: "Sign In",
288 | 											value: "sign-in",
289 | 											content: <SignIn />,
290 | 										},
291 | 										{
292 | 											title: "Sign Up",
293 | 											value: "sign-up",
294 | 											content: <SignUp />,
295 | 										},
296 | 									]}
297 | 								/>
298 | 							) : (
299 | 								<SignIn />
300 | 							)}
301 | 						</div>
302 | 					</div>
303 | 					<ScrollArea
304 | 						className="w-[45%] flex-grow"
305 | 						style={{
306 | 							scrollbarWidth: "none",
307 | 							scrollbarColor: "transparent transparent",
308 | 							//@ts-expect-error
309 | 							"&::-webkit-scrollbar": {
310 | 								display: "none",
311 | 							},
312 | 						}}
313 | 					>
314 | 						<div className="h-[580px]">
315 | 							{currentStep === 0 ? (
316 | 								<Card className="rounded-none flex-grow h-full">
317 | 									<CardHeader className="flex flex-row justify-between">
318 | 										<CardTitle>Configuration</CardTitle>
319 | 										<div
320 | 											className="cursor-pointer"
321 | 											onClick={() => {
322 | 												if (resolvedTheme === "dark") {
323 | 													setTheme("light");
324 | 												} else {
325 | 													setTheme("dark");
326 | 												}
327 | 											}}
328 | 										>
329 | 											{resolvedTheme === "dark" ? (
330 | 												<Moon onClick={() => setTheme("light")} size={18} />
331 | 											) : (
332 | 												<Sun onClick={() => setTheme("dark")} size={18} />
333 | 											)}
334 | 										</div>
335 | 									</CardHeader>
336 | 									<CardContent className="max-h-[400px] overflow-scroll">
337 | 										<div className="flex flex-col gap-2">
338 | 											<div>
339 | 												<Label>Email & Password</Label>
340 | 											</div>
341 | 											<Separator />
342 | 											<div className="flex items-center justify-between">
343 | 												<div className="flex items-center">
344 | 													<Label
345 | 														className="cursor-pointer"
346 | 														htmlFor="email-provider-email"
347 | 													>
348 | 														Enabled
349 | 													</Label>
350 | 												</div>
351 | 												<Switch
352 | 													id="email-provider-email"
353 | 													checked={options.email}
354 | 													onCheckedChange={(checked) => {
355 | 														setOptions((prev) => ({
356 | 															...prev,
357 | 															email: checked,
358 | 															magicLink: checked ? false : prev.magicLink,
359 | 															signUp: checked,
360 | 														}));
361 | 													}}
362 | 												/>
363 | 											</div>
364 | 											<div className="flex items-center justify-between">
365 | 												<div className="flex items-center gap-2">
366 | 													<Label
367 | 														className="cursor-pointer"
368 | 														htmlFor="email-provider-remember-me"
369 | 													>
370 | 														Remember Me
371 | 													</Label>
372 | 												</div>
373 | 												<Switch
374 | 													id="email-provider-remember-me"
375 | 													checked={options.rememberMe}
376 | 													onCheckedChange={(checked) => {
377 | 														setOptions((prev) => ({
378 | 															...prev,
379 | 															rememberMe: checked,
380 | 														}));
381 | 													}}
382 | 												/>
383 | 											</div>
384 | 											<div className="flex items-center justify-between">
385 | 												<div className="flex items-center gap-2">
386 | 													<Label
387 | 														className="cursor-pointer"
388 | 														htmlFor="email-provider-forget-password"
389 | 													>
390 | 														Forget Password
391 | 													</Label>
392 | 												</div>
393 | 												<Switch
394 | 													id="email-provider-forget-password"
395 | 													checked={options.requestPasswordReset}
396 | 													onCheckedChange={(checked) => {
397 | 														setOptions((prev) => ({
398 | 															...prev,
399 | 															requestPasswordReset: checked,
400 | 														}));
401 | 													}}
402 | 												/>
403 | 											</div>
404 | 										</div>
405 | 										<div className="flex flex-col gap-2 mt-4">
406 | 											<div>
407 | 												<Label>Social Providers</Label>
408 | 											</div>
409 | 											<Separator />
410 | 											{Object.entries(socialProviders).map(
411 | 												([provider, { Icon }]) => (
412 | 													<div
413 | 														className="flex items-center justify-between"
414 | 														key={provider}
415 | 													>
416 | 														<div className="flex items-center gap-2">
417 | 															<Icon />
418 | 															<Label
419 | 																className="cursor-pointer"
420 | 																htmlFor={"social-provider".concat(
421 | 																	"-",
422 | 																	provider,
423 | 																)}
424 | 															>
425 | 																{provider.charAt(0).toUpperCase() +
426 | 																	provider.slice(1)}
427 | 															</Label>
428 | 														</div>
429 | 														<Switch
430 | 															id={"social-provider".concat("-", provider)}
431 | 															checked={options.socialProviders.includes(
432 | 																provider,
433 | 															)}
434 | 															onCheckedChange={(checked) => {
435 | 																setOptions((prev) => ({
436 | 																	...prev,
437 | 																	socialProviders: checked
438 | 																		? [...prev.socialProviders, provider]
439 | 																		: prev.socialProviders.filter(
440 | 																				(p) => p !== provider,
441 | 																			),
442 | 																}));
443 | 															}}
444 | 														/>
445 | 													</div>
446 | 												),
447 | 											)}
448 | 										</div>
449 | 										<div className="flex flex-col gap-2 mt-4">
450 | 											<div>
451 | 												<Label>Plugins</Label>
452 | 											</div>
453 | 											<Separator />
454 | 											<div className="flex items-center justify-between">
455 | 												<div className="flex items-center gap-2">
456 | 													<svg
457 | 														xmlns="http://www.w3.org/2000/svg"
458 | 														width="1em"
459 | 														height="1em"
460 | 														viewBox="0 0 24 24"
461 | 													>
462 | 														<path
463 | 															fill="currentColor"
464 | 															d="M5 20q-.825 0-1.412-.587T3 18v-.8q0-.85.438-1.562T4.6 14.55q1.55-.775 3.15-1.162T11 13q.35 0 .7.013t.7.062q.275.025.437.213t.163.462q.05 1.175.575 2.213t1.4 1.762q.175.125.275.313t.1.412V19q0 .425-.288.713T14.35 20zm6-8q-1.65 0-2.825-1.175T7 8t1.175-2.825T11 4t2.825 1.175T15 8t-1.175 2.825T11 12m7.5 2q.425 0 .713-.288T19.5 13t-.288-.712T18.5 12t-.712.288T17.5 13t.288.713t.712.287m.15 8.65l-1-1q-.05-.05-.15-.35v-4.45q-1.1-.325-1.8-1.237T15 13.5q0-1.45 1.025-2.475T18.5 10t2.475 1.025T22 13.5q0 1.125-.638 2t-1.612 1.25l.9.9q.15.15.15.35t-.15.35l-.8.8q-.15.15-.15.35t.15.35l.8.8q.15.15.15.35t-.15.35l-1.3 1.3q-.15.15-.35.15t-.35-.15"
465 | 														></path>
466 | 													</svg>
467 | 													<Label
468 | 														className="cursor-pointer"
469 | 														htmlFor="plugin-passkey"
470 | 													>
471 | 														Passkey
472 | 													</Label>
473 | 												</div>
474 | 												<Switch
475 | 													id="plugin-passkey"
476 | 													checked={options.passkey}
477 | 													onCheckedChange={(checked) => {
478 | 														setOptions((prev) => ({
479 | 															...prev,
480 | 															passkey: checked,
481 | 														}));
482 | 													}}
483 | 												/>
484 | 											</div>
485 | 
486 | 											<div className="flex items-center justify-between">
487 | 												<div className="flex items-center gap-2">
488 | 													<svg
489 | 														xmlns="http://www.w3.org/2000/svg"
490 | 														width="1em"
491 | 														height="1em"
492 | 														viewBox="0 0 24 24"
493 | 													>
494 | 														<g fill="none">
495 | 															<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
496 | 															<path
497 | 																fill="currentColor"
498 | 																d="M17.5 3a4.5 4.5 0 0 1 4.495 4.288L22 7.5V15a2 2 0 0 1-1.85 1.995L20 17h-3v3a1 1 0 0 1-1.993.117L15 20v-3H4a2 2 0 0 1-1.995-1.85L2 15V7.5a4.5 4.5 0 0 1 4.288-4.495L6.5 3zm-11 2A2.5 2.5 0 0 0 4 7.5V15h5V7.5A2.5 2.5 0 0 0 6.5 5M7 8a1 1 0 0 1 .117 1.993L7 10H6a1 1 0 0 1-.117-1.993L6 8z"
499 | 															></path>
500 | 														</g>
501 | 													</svg>
502 | 													<Label
503 | 														className="cursor-pointer"
504 | 														htmlFor="plugin-otp-magic-link"
505 | 													>
506 | 														Magic Link
507 | 													</Label>
508 | 												</div>
509 | 												<Switch
510 | 													id="plugin-otp-magic-link"
511 | 													checked={options.magicLink}
512 | 													onCheckedChange={(checked) => {
513 | 														setOptions((prev) => ({
514 | 															...prev,
515 | 															magicLink: checked,
516 | 															email: checked ? false : prev.email,
517 | 															signUp: checked ? false : prev.signUp,
518 | 														}));
519 | 													}}
520 | 												/>
521 | 											</div>
522 | 										</div>
523 | 										<div className="mt-4">
524 | 											<Separator />
525 | 											<div className="flex items-center justify-between mt-2">
526 | 												<Label
527 | 													className="cursor-pointer"
528 | 													htmlFor="label-powered-by"
529 | 												>
530 | 													Show Built with label
531 | 												</Label>
532 | 												<Switch
533 | 													id="label-powered-by"
534 | 													checked={options.label}
535 | 													onCheckedChange={(checked) => {
536 | 														setOptions((prev) => ({
537 | 															...prev,
538 | 															label: checked,
539 | 														}));
540 | 													}}
541 | 												/>
542 | 											</div>
543 | 										</div>
544 | 									</CardContent>
545 | 									<CardFooter>
546 | 										<button
547 | 											className="bg-stone-950 no-underline group cursor-pointer relative shadow-2xl shadow-zinc-900 rounded-sm p-px text-xs font-semibold leading-6  text-white inline-block w-full"
548 | 											onClick={() => {
549 | 												setCurrentStep(currentStep + 1);
550 | 											}}
551 | 										>
552 | 											<span className="absolute inset-0 overflow-hidden rounded-sm">
553 | 												<span className="absolute inset-0 rounded-sm bg-[image:radial-gradient(75%_100%_at_50%_0%,rgba(56,189,248,0.6)_0%,rgba(56,189,248,0)_75%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100"></span>
554 | 											</span>
555 | 											<div className="relative flex space-x-2 items-center z-10 rounded-none bg-zinc-950 py-2 px-4 ring-1 ring-white/10 justify-center">
556 | 												<span>Continue</span>
557 | 											</div>
558 | 											<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-stone-800/90 to-emerald-400/0 transition-opacity duration-500 group-hover:opacity-40"></span>
559 | 										</button>
560 | 									</CardFooter>
561 | 								</Card>
562 | 							) : currentStep === 1 ? (
563 | 								<Card className="rounded-none flex-grow  h-full">
564 | 									<CardHeader>
565 | 										<CardTitle>Choose Framework</CardTitle>
566 | 										<p
567 | 											className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
568 | 											onClick={() => {
569 | 												setCurrentStep(0);
570 | 											}}
571 | 										>
572 | 											Go Back
573 | 										</p>
574 | 									</CardHeader>
575 | 									<CardContent className="flex items-start gap-2 flex-wrap justify-between">
576 | 										{frameworks.map((fm) => (
577 | 											<div
578 | 												onClick={() => {
579 | 													if (fm.title === "Next.js") {
580 | 														setCurrentStep(currentStep + 1);
581 | 													}
582 | 												}}
583 | 												className={cn(
584 | 													"flex flex-col items-center gap-4 border p-6 rounded-md w-5/12 flex-grow h-44 relative",
585 | 													fm.title !== "Next.js"
586 | 														? "opacity-55"
587 | 														: "hover:ring-1 transition-all ring-border hover:bg-background duration-200 ease-in-out cursor-pointer",
588 | 												)}
589 | 												key={fm.title}
590 | 											>
591 | 												{fm.title !== "Next.js" && (
592 | 													<span className="absolute top-4 right-4 text-xs">
593 | 														Coming Soon
594 | 													</span>
595 | 												)}
596 | 												<fm.Icon />
597 | 												<Label className="text-2xl">{fm.title}</Label>
598 | 												<p className="text-sm">{fm.description}</p>
599 | 											</div>
600 | 										))}
601 | 									</CardContent>
602 | 								</Card>
603 | 							) : (
604 | 								<Card className="rounded-none w-full overflow-y-hidden h-full overflow-auto">
605 | 									<CardHeader>
606 | 										<div className="flex flex-col -mb-2 items-start">
607 | 											<CardTitle>Code</CardTitle>
608 | 										</div>
609 | 									</CardHeader>
610 | 									<CardContent>
611 | 										<div className="flex gap-2 items-baseline">
612 | 											<p>
613 | 												Copy the code below and paste it in your application to
614 | 												get started.
615 | 											</p>
616 | 											<p
617 | 												className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
618 | 												onClick={() => {
619 | 													setCurrentStep(0);
620 | 												}}
621 | 											>
622 | 												Go Back
623 | 											</p>
624 | 										</div>
625 | 										<div>
626 | 											<CodeTabs />
627 | 										</div>
628 | 									</CardContent>
629 | 								</Card>
630 | 							)}
631 | 						</div>
632 | 					</ScrollArea>
633 | 				</div>
634 | 			</DialogContent>
635 | 		</Dialog>
636 | 	);
637 | }
638 | 
```

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

```typescript
  1 | import { afterAll, beforeAll, describe, expect, it } from "vitest";
  2 | import { genericOAuth } from ".";
  3 | import { createAuthClient } from "../../client";
  4 | import { getTestInstance } from "../../test-utils/test-instance";
  5 | import { genericOAuthClient } from "./client";
  6 | 
  7 | import { betterFetch } from "@better-fetch/fetch";
  8 | import { OAuth2Server } from "oauth2-mock-server";
  9 | import { parseSetCookieHeader } from "../../cookies";
 10 | 
 11 | describe("oauth2", async () => {
 12 | 	const providerId = "test";
 13 | 	const clientId = "test-client-id";
 14 | 	const clientSecret = "test-client-secret";
 15 | 	const server = new OAuth2Server();
 16 | 	await server.start();
 17 | 	const port = Number(server.issuer.url?.split(":")[2]!);
 18 | 
 19 | 	afterAll(async () => {
 20 | 		await server.stop();
 21 | 	});
 22 | 
 23 | 	const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
 24 | 		plugins: [
 25 | 			genericOAuth({
 26 | 				config: [
 27 | 					{
 28 | 						providerId,
 29 | 						discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
 30 | 						clientId: clientId,
 31 | 						clientSecret: clientSecret,
 32 | 						pkce: true,
 33 | 					},
 34 | 				],
 35 | 			}),
 36 | 		],
 37 | 	});
 38 | 
 39 | 	const authClient = createAuthClient({
 40 | 		plugins: [genericOAuthClient()],
 41 | 		baseURL: "http://localhost:3000",
 42 | 		fetchOptions: {
 43 | 			customFetchImpl,
 44 | 		},
 45 | 	});
 46 | 
 47 | 	beforeAll(async () => {
 48 | 		const context = await auth.$context;
 49 | 		await context.internalAdapter.createUser({
 50 | 			email: "[email protected]",
 51 | 			name: "OAuth2 Test",
 52 | 		});
 53 | 		await server.issuer.keys.generate("RS256");
 54 | 	});
 55 | 
 56 | 	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
 57 | 		userInfoResponse.body = {
 58 | 			email: "[email protected]",
 59 | 			name: "OAuth2 Test",
 60 | 			sub: "oauth2",
 61 | 			picture: "https://test.com/picture.png",
 62 | 			email_verified: true,
 63 | 		};
 64 | 		userInfoResponse.statusCode = 200;
 65 | 	});
 66 | 
 67 | 	async function simulateOAuthFlow(
 68 | 		authUrl: string,
 69 | 		headers: Headers,
 70 | 		fetchImpl?: (...args: any) => any,
 71 | 	) {
 72 | 		let location: string | null = null;
 73 | 		await betterFetch(authUrl, {
 74 | 			method: "GET",
 75 | 			redirect: "manual",
 76 | 			onError(context) {
 77 | 				location = context.response.headers.get("location");
 78 | 			},
 79 | 		});
 80 | 
 81 | 		if (!location) throw new Error("No redirect location found");
 82 | 
 83 | 		let callbackURL = "";
 84 | 		const newHeaders = new Headers();
 85 | 		await betterFetch(location, {
 86 | 			method: "GET",
 87 | 			customFetchImpl: fetchImpl || customFetchImpl,
 88 | 			headers,
 89 | 			onError(context) {
 90 | 				callbackURL = context.response.headers.get("location") || "";
 91 | 				cookieSetter(newHeaders)(context);
 92 | 			},
 93 | 		});
 94 | 
 95 | 		return { callbackURL, headers: newHeaders };
 96 | 	}
 97 | 
 98 | 	it("should redirect to the provider and handle the response", async () => {
 99 | 		let headers = new Headers();
100 | 		const signInRes = await authClient.signIn.oauth2({
101 | 			providerId: "test",
102 | 			callbackURL: "http://localhost:3000/dashboard",
103 | 			newUserCallbackURL: "http://localhost:3000/new_user",
104 | 			fetchOptions: {
105 | 				onSuccess: cookieSetter(headers),
106 | 			},
107 | 		});
108 | 		expect(signInRes.data).toMatchObject({
109 | 			url: expect.stringContaining(`http://localhost:${port}/authorize`),
110 | 			redirect: true,
111 | 		});
112 | 		const { callbackURL } = await simulateOAuthFlow(
113 | 			signInRes.data?.url || "",
114 | 			headers,
115 | 		);
116 | 		expect(callbackURL).toBe("http://localhost:3000/dashboard");
117 | 	});
118 | 
119 | 	it("should redirect to the provider and handle the response for a new user", async () => {
120 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
121 | 			userInfoResponse.body = {
122 | 				email: "[email protected]",
123 | 				name: "OAuth2 Test 2",
124 | 				sub: "oauth2-2",
125 | 				picture: "https://test.com/picture.png",
126 | 				email_verified: true,
127 | 			};
128 | 			userInfoResponse.statusCode = 200;
129 | 		});
130 | 
131 | 		let headers = new Headers();
132 | 		const signInRes = await authClient.signIn.oauth2({
133 | 			providerId: "test",
134 | 			callbackURL: "http://localhost:3000/dashboard",
135 | 			newUserCallbackURL: "http://localhost:3000/new_user",
136 | 			fetchOptions: {
137 | 				onSuccess: cookieSetter(headers),
138 | 			},
139 | 		});
140 | 		expect(signInRes.data).toMatchObject({
141 | 			url: expect.stringContaining(`http://localhost:${port}/authorize`),
142 | 			redirect: true,
143 | 		});
144 | 		const { callbackURL, headers: newHeaders } = await simulateOAuthFlow(
145 | 			signInRes.data?.url || "",
146 | 			headers,
147 | 		);
148 | 		expect(callbackURL).toBe("http://localhost:3000/new_user");
149 | 		const session = await authClient.getSession({
150 | 			fetchOptions: {
151 | 				headers: newHeaders,
152 | 			},
153 | 		});
154 | 		console.log(session.data, newHeaders);
155 | 		const ctx = await auth.$context;
156 | 		const accounts = await ctx.internalAdapter.findAccounts(
157 | 			session.data?.user.id!,
158 | 		);
159 | 		const account = accounts[0];
160 | 		expect(account).toMatchObject({
161 | 			providerId,
162 | 			accountId: "oauth2-2",
163 | 			userId: session.data?.user.id,
164 | 			accessToken: expect.any(String),
165 | 			refreshToken: expect.any(String),
166 | 			accessTokenExpiresAt: expect.any(Date),
167 | 			refreshTokenExpiresAt: null,
168 | 			scope: expect.any(String),
169 | 			idToken: expect.any(String),
170 | 		});
171 | 	});
172 | 
173 | 	it("should redirect to the provider and handle the response after linked", async () => {
174 | 		let headers = new Headers();
175 | 		const res = await authClient.signIn.oauth2({
176 | 			providerId: "test",
177 | 			callbackURL: "http://localhost:3000/dashboard",
178 | 			newUserCallbackURL: "http://localhost:3000/new_user",
179 | 			fetchOptions: {
180 | 				onSuccess: cookieSetter(headers),
181 | 			},
182 | 		});
183 | 		const { callbackURL } = await simulateOAuthFlow(
184 | 			res.data?.url || "",
185 | 			headers,
186 | 		);
187 | 		expect(callbackURL).toBe("http://localhost:3000/dashboard");
188 | 	});
189 | 
190 | 	it("should handle invalid provider ID", async () => {
191 | 		const res = await authClient.signIn.oauth2({
192 | 			providerId: "invalid-provider",
193 | 			callbackURL: "http://localhost:3000/dashboard",
194 | 			newUserCallbackURL: "http://localhost:3000/new_user",
195 | 		});
196 | 		expect(res.error?.status).toBe(400);
197 | 	});
198 | 
199 | 	it("should handle server error during OAuth flow", async () => {
200 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
201 | 			userInfoResponse.body = {
202 | 				email: "[email protected]",
203 | 				name: "OAuth2 Test",
204 | 				sub: "oauth2",
205 | 				picture: "https://test.com/picture.png",
206 | 				email_verified: true,
207 | 			};
208 | 			userInfoResponse.statusCode = 500;
209 | 		});
210 | 
211 | 		let headers = new Headers();
212 | 		const res = await authClient.signIn.oauth2(
213 | 			{
214 | 				providerId: "test",
215 | 				callbackURL: "http://localhost:3000/dashboard",
216 | 				newUserCallbackURL: "http://localhost:3000/new_user",
217 | 			},
218 | 			{
219 | 				onSuccess(context) {
220 | 					const parsedSetCookie = parseSetCookieHeader(
221 | 						context.response.headers.get("Set-Cookie") || "",
222 | 					);
223 | 					headers.set(
224 | 						"cookie",
225 | 						`better-auth.state=${
226 | 							parsedSetCookie.get("better-auth.state")?.value
227 | 						}; better-auth.pk_code_verifier=${
228 | 							parsedSetCookie.get("better-auth.pk_code_verifier")?.value
229 | 						}`,
230 | 					);
231 | 				},
232 | 			},
233 | 		);
234 | 
235 | 		const { callbackURL } = await simulateOAuthFlow(
236 | 			res.data?.url || "",
237 | 			headers,
238 | 		);
239 | 		expect(callbackURL).toContain("?error=");
240 | 	});
241 | 
242 | 	it("should work with custom redirect uri", async () => {
243 | 		const { customFetchImpl, auth } = await getTestInstance({
244 | 			plugins: [
245 | 				genericOAuth({
246 | 					config: [
247 | 						{
248 | 							providerId: "test2",
249 | 							discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
250 | 							clientId: clientId,
251 | 							clientSecret: clientSecret,
252 | 							redirectURI: "http://localhost:3000/api/auth/callback/test2",
253 | 							pkce: true,
254 | 						},
255 | 					],
256 | 				}),
257 | 			],
258 | 		});
259 | 		const headers = new Headers();
260 | 		const authClient = createAuthClient({
261 | 			plugins: [genericOAuthClient()],
262 | 			baseURL: "http://localhost:3000",
263 | 			fetchOptions: {
264 | 				customFetchImpl,
265 | 				onSuccess: cookieSetter(headers),
266 | 			},
267 | 		});
268 | 
269 | 		const res = await authClient.signIn.oauth2({
270 | 			providerId: "test2",
271 | 			callbackURL: "http://localhost:3000/dashboard",
272 | 			newUserCallbackURL: "http://localhost:3000/new_user",
273 | 			fetchOptions: {
274 | 				onSuccess: cookieSetter(headers),
275 | 			},
276 | 		});
277 | 		expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
278 | 		const { callbackURL } = await simulateOAuthFlow(
279 | 			res.data?.url || "",
280 | 			headers,
281 | 			customFetchImpl,
282 | 		);
283 | 		expect(callbackURL).toBe("http://localhost:3000/new_user");
284 | 	});
285 | 
286 | 	it("should not create user when sign ups are disabled", async () => {
287 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
288 | 			userInfoResponse.body = {
289 | 				email: "[email protected]",
290 | 				name: "OAuth2 Test Signup Disabled",
291 | 				sub: "oauth2-signup-disabled",
292 | 				picture: "https://test.com/picture.png",
293 | 				email_verified: true,
294 | 			};
295 | 			userInfoResponse.statusCode = 200;
296 | 		});
297 | 
298 | 		const { customFetchImpl, cookieSetter } = await getTestInstance({
299 | 			plugins: [
300 | 				genericOAuth({
301 | 					config: [
302 | 						{
303 | 							providerId: "test2",
304 | 							discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
305 | 							clientId: clientId,
306 | 							clientSecret: clientSecret,
307 | 							pkce: true,
308 | 							disableImplicitSignUp: true,
309 | 						},
310 | 					],
311 | 				}),
312 | 			],
313 | 		});
314 | 		const authClient = createAuthClient({
315 | 			plugins: [genericOAuthClient()],
316 | 			baseURL: "http://localhost:3000",
317 | 			fetchOptions: {
318 | 				customFetchImpl,
319 | 			},
320 | 		});
321 | 		const headers = new Headers();
322 | 		const res = await authClient.signIn.oauth2({
323 | 			providerId: "test2",
324 | 			callbackURL: "http://localhost:3000/dashboard",
325 | 			errorCallbackURL: "http://localhost:3000/error",
326 | 			fetchOptions: {
327 | 				onSuccess: cookieSetter(headers),
328 | 			},
329 | 		});
330 | 		expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
331 | 		const { callbackURL } = await simulateOAuthFlow(
332 | 			res.data?.url || "",
333 | 			headers,
334 | 			customFetchImpl,
335 | 		);
336 | 		expect(callbackURL).toBe(
337 | 			"http://localhost:3000/error?error=signup_disabled",
338 | 		);
339 | 	});
340 | 
341 | 	it("should create user when sign ups are disabled and sign up is requested", async () => {
342 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
343 | 			userInfoResponse.body = {
344 | 				email: "[email protected]",
345 | 				name: "OAuth2 Test Signup Disabled And Requested",
346 | 				sub: "oauth2-signup-disabled-and-requested",
347 | 				picture: "https://test.com/picture.png",
348 | 				email_verified: true,
349 | 			};
350 | 			userInfoResponse.statusCode = 200;
351 | 		});
352 | 
353 | 		const { customFetchImpl, cookieSetter } = await getTestInstance({
354 | 			plugins: [
355 | 				genericOAuth({
356 | 					config: [
357 | 						{
358 | 							providerId: "test2",
359 | 							discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
360 | 							clientId: clientId,
361 | 							clientSecret: clientSecret,
362 | 							pkce: true,
363 | 							disableImplicitSignUp: true,
364 | 						},
365 | 					],
366 | 				}),
367 | 			],
368 | 		});
369 | 
370 | 		const authClient = createAuthClient({
371 | 			plugins: [genericOAuthClient()],
372 | 			baseURL: "http://localhost:3000",
373 | 			fetchOptions: {
374 | 				customFetchImpl,
375 | 			},
376 | 		});
377 | 		const headers = new Headers();
378 | 		const res = await authClient.signIn.oauth2({
379 | 			providerId: "test2",
380 | 			callbackURL: "http://localhost:3000/dashboard",
381 | 			errorCallbackURL: "http://localhost:3000/error",
382 | 			requestSignUp: true,
383 | 			fetchOptions: {
384 | 				onSuccess: cookieSetter(headers),
385 | 			},
386 | 		});
387 | 		expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
388 | 		const { callbackURL } = await simulateOAuthFlow(
389 | 			res.data?.url || "",
390 | 			headers,
391 | 			customFetchImpl,
392 | 		);
393 | 		expect(callbackURL).toBe("http://localhost:3000/dashboard");
394 | 	});
395 | 
396 | 	it("should pass authorization headers in oAuth2Callback", async () => {
397 | 		const customHeaders = {
398 | 			"X-Custom-Header": "test-value",
399 | 		};
400 | 
401 | 		let receivedHeaders: Record<string, string> = {};
402 | 		server.service.once("beforeTokenSigning", (token, req) => {
403 | 			receivedHeaders = req.headers as Record<string, string>;
404 | 		});
405 | 
406 | 		const { customFetchImpl, cookieSetter } = await getTestInstance({
407 | 			plugins: [
408 | 				genericOAuth({
409 | 					config: [
410 | 						{
411 | 							providerId: "test3",
412 | 							discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
413 | 							clientId: clientId,
414 | 							clientSecret: clientSecret,
415 | 							pkce: true,
416 | 							authorizationHeaders: customHeaders,
417 | 						},
418 | 					],
419 | 				}),
420 | 			],
421 | 		});
422 | 		const headers = new Headers();
423 | 		const authClient = createAuthClient({
424 | 			plugins: [genericOAuthClient()],
425 | 			baseURL: "http://localhost:3000",
426 | 			fetchOptions: {
427 | 				customFetchImpl,
428 | 				onSuccess: cookieSetter(headers),
429 | 			},
430 | 		});
431 | 
432 | 		const res = await authClient.signIn.oauth2({
433 | 			providerId: "test3",
434 | 			callbackURL: "http://localhost:3000/dashboard",
435 | 			newUserCallbackURL: "http://localhost:3000/new_user",
436 | 			fetchOptions: {
437 | 				onSuccess: cookieSetter(headers),
438 | 			},
439 | 		});
440 | 
441 | 		expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
442 | 		await simulateOAuthFlow(res.data?.url || "", headers, customFetchImpl);
443 | 
444 | 		expect(receivedHeaders).toHaveProperty("x-custom-header");
445 | 		expect(receivedHeaders["x-custom-header"]).toBe("test-value");
446 | 	});
447 | 
448 | 	it("should delete oauth user with verification flow without password", async () => {
449 | 		let token = "";
450 | 		const { customFetchImpl, cookieSetter } = await getTestInstance({
451 | 			user: {
452 | 				deleteUser: {
453 | 					enabled: true,
454 | 					async sendDeleteAccountVerification(data, _) {
455 | 						token = data.token;
456 | 					},
457 | 				},
458 | 			},
459 | 			plugins: [
460 | 				genericOAuth({
461 | 					config: [
462 | 						{
463 | 							providerId: "test",
464 | 							discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
465 | 							clientId: clientId,
466 | 							clientSecret: clientSecret,
467 | 						},
468 | 					],
469 | 				}),
470 | 			],
471 | 		});
472 | 		const headers = new Headers();
473 | 		const client = createAuthClient({
474 | 			plugins: [genericOAuthClient()],
475 | 			baseURL: "http://localhost:3000",
476 | 			fetchOptions: {
477 | 				customFetchImpl,
478 | 				onSuccess: cookieSetter(headers),
479 | 			},
480 | 		});
481 | 		const signInRes = await client.signIn.oauth2({
482 | 			providerId: "test",
483 | 			callbackURL: "http://localhost:3000/dashboard",
484 | 			newUserCallbackURL: "http://localhost:3000/new_user",
485 | 			fetchOptions: {
486 | 				onSuccess: cookieSetter(headers),
487 | 			},
488 | 		});
489 | 
490 | 		expect(signInRes.data).toMatchObject({
491 | 			url: expect.stringContaining(`http://localhost:${port}/authorize`),
492 | 			redirect: true,
493 | 		});
494 | 
495 | 		const { headers: newHeaders } = await simulateOAuthFlow(
496 | 			signInRes.data?.url || "",
497 | 			headers,
498 | 			customFetchImpl,
499 | 		);
500 | 
501 | 		const session = await client.getSession({
502 | 			fetchOptions: {
503 | 				headers: newHeaders,
504 | 			},
505 | 		});
506 | 		expect(session.data).not.toBeNull();
507 | 
508 | 		const deleteRes = await client.deleteUser({
509 | 			fetchOptions: {
510 | 				headers: newHeaders,
511 | 			},
512 | 		});
513 | 
514 | 		expect(deleteRes.data).toMatchObject({
515 | 			success: true,
516 | 		});
517 | 
518 | 		expect(token.length).toBe(32);
519 | 
520 | 		const deleteCallbackRes = await client.deleteUser({
521 | 			token,
522 | 			fetchOptions: {
523 | 				headers: newHeaders,
524 | 			},
525 | 		});
526 | 		expect(deleteCallbackRes.data).toMatchObject({
527 | 			success: true,
528 | 		});
529 | 		const nullSession = await client.getSession({
530 | 			fetchOptions: {
531 | 				headers,
532 | 			},
533 | 		});
534 | 		expect(nullSession.data).toBeNull();
535 | 	});
536 | 
537 | 	it("should handle numeric account IDs correctly and prevent duplicate accounts", async () => {
538 | 		const numericAccountId = 123456789;
539 | 		const userEmail = "[email protected]";
540 | 
541 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
542 | 			userInfoResponse.body = {
543 | 				email: userEmail,
544 | 				name: "Numeric ID Test User",
545 | 				sub: numericAccountId,
546 | 				picture: "https://test.com/picture.png",
547 | 				email_verified: true,
548 | 			};
549 | 			userInfoResponse.statusCode = 200;
550 | 		});
551 | 
552 | 		const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
553 | 			plugins: [
554 | 				genericOAuth({
555 | 					config: [
556 | 						{
557 | 							providerId: "numeric-test",
558 | 							discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
559 | 							clientId: clientId,
560 | 							clientSecret: clientSecret,
561 | 							pkce: true,
562 | 						},
563 | 					],
564 | 				}),
565 | 			],
566 | 		});
567 | 		const headers = new Headers();
568 | 		const authClient = createAuthClient({
569 | 			plugins: [genericOAuthClient()],
570 | 			baseURL: "http://localhost:3000",
571 | 			fetchOptions: {
572 | 				customFetchImpl,
573 | 				onSuccess: cookieSetter(headers),
574 | 			},
575 | 		});
576 | 
577 | 		const firstSignIn = await authClient.signIn.oauth2({
578 | 			providerId: "numeric-test",
579 | 			callbackURL: "http://localhost:3000/dashboard",
580 | 			newUserCallbackURL: "http://localhost:3000/new_user",
581 | 		});
582 | 
583 | 		const { callbackURL: firstCallbackURL, headers: firstHeaders } =
584 | 			await simulateOAuthFlow(
585 | 				firstSignIn.data?.url || "",
586 | 				headers,
587 | 				customFetchImpl,
588 | 			);
589 | 
590 | 		expect(firstCallbackURL).toBe("http://localhost:3000/new_user");
591 | 
592 | 		const firstSession = await authClient.getSession({
593 | 			fetchOptions: {
594 | 				headers: firstHeaders,
595 | 			},
596 | 		});
597 | 
598 | 		expect(firstSession.data).not.toBeNull();
599 | 		const userId = firstSession.data?.user.id!;
600 | 
601 | 		const ctx = await auth.$context;
602 | 		const accountsAfterFirst = await ctx.internalAdapter.findAccounts(userId);
603 | 		expect(accountsAfterFirst).toHaveLength(1);
604 | 		expect(accountsAfterFirst[0]).toMatchObject({
605 | 			providerId: "numeric-test",
606 | 			accountId: String(numericAccountId),
607 | 			userId: userId,
608 | 		});
609 | 
610 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
611 | 			userInfoResponse.body = {
612 | 				email: userEmail,
613 | 				name: "Numeric ID Test User",
614 | 				sub: numericAccountId,
615 | 				picture: "https://test.com/picture.png",
616 | 				email_verified: true,
617 | 			};
618 | 			userInfoResponse.statusCode = 200;
619 | 		});
620 | 
621 | 		const secondSignIn = await authClient.signIn.oauth2({
622 | 			providerId: "numeric-test",
623 | 			callbackURL: "http://localhost:3000/dashboard",
624 | 		});
625 | 
626 | 		const { callbackURL: secondCallbackURL, headers: secondHeaders } =
627 | 			await simulateOAuthFlow(
628 | 				secondSignIn.data?.url || "",
629 | 				headers,
630 | 				customFetchImpl,
631 | 			);
632 | 
633 | 		expect(secondCallbackURL).toBe("http://localhost:3000/dashboard");
634 | 
635 | 		const secondSession = await authClient.getSession({
636 | 			fetchOptions: {
637 | 				headers: secondHeaders,
638 | 			},
639 | 		});
640 | 
641 | 		expect(secondSession.data).not.toBeNull();
642 | 		expect(secondSession.data?.user.id).toBe(userId);
643 | 
644 | 		const accountsAfterSecond = await ctx.internalAdapter.findAccounts(userId);
645 | 		expect(accountsAfterSecond).toHaveLength(1);
646 | 		expect(accountsAfterSecond[0]!.accountId).toBe(String(numericAccountId));
647 | 	});
648 | 
649 | 	it("should handle custom getUserInfo returning numeric ID", async () => {
650 | 		const numericId = 987654321;
651 | 
652 | 		const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
653 | 			plugins: [
654 | 				genericOAuth({
655 | 					config: [
656 | 						{
657 | 							providerId: "custom-numeric",
658 | 							authorizationUrl: `http://localhost:${port}/authorize`,
659 | 							tokenUrl: `http://localhost:${port}/token`,
660 | 							clientId: clientId,
661 | 							clientSecret: clientSecret,
662 | 							pkce: true,
663 | 							getUserInfo: async (_tokens) => {
664 | 								return {
665 | 									id: numericId,
666 | 									email: "[email protected]",
667 | 									name: "Custom Numeric User",
668 | 									emailVerified: true,
669 | 									image: "https://test.com/avatar.png",
670 | 								};
671 | 							},
672 | 						},
673 | 					],
674 | 				}),
675 | 			],
676 | 		});
677 | 		const headers = new Headers();
678 | 		const authClient = createAuthClient({
679 | 			plugins: [genericOAuthClient()],
680 | 			baseURL: "http://localhost:3000",
681 | 			fetchOptions: {
682 | 				customFetchImpl,
683 | 				onSuccess: cookieSetter(headers),
684 | 			},
685 | 		});
686 | 
687 | 		const signInRes = await authClient.signIn.oauth2({
688 | 			providerId: "custom-numeric",
689 | 			callbackURL: "http://localhost:3000/dashboard",
690 | 			newUserCallbackURL: "http://localhost:3000/new_user",
691 | 		});
692 | 
693 | 		const { callbackURL, headers: newHeaders } = await simulateOAuthFlow(
694 | 			signInRes.data?.url || "",
695 | 			headers,
696 | 			customFetchImpl,
697 | 		);
698 | 
699 | 		expect(callbackURL).toBe("http://localhost:3000/new_user");
700 | 
701 | 		const session = await authClient.getSession({
702 | 			fetchOptions: {
703 | 				headers: newHeaders,
704 | 			},
705 | 		});
706 | 
707 | 		const ctx = await auth.$context;
708 | 		const accounts = await ctx.internalAdapter.findAccounts(
709 | 			session.data?.user.id!,
710 | 		);
711 | 
712 | 		expect(accounts[0]!.accountId).toBe(String(numericId));
713 | 	});
714 | 
715 | 	it("should handle mapProfileToUser returning numeric ID", async () => {
716 | 		const numericProfileId = 111222333;
717 | 
718 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
719 | 			userInfoResponse.body = {
720 | 				email: "[email protected]",
721 | 				name: "Map Profile Numeric User",
722 | 				sub: "string-sub-id",
723 | 				user_id: numericProfileId,
724 | 				picture: "https://test.com/picture.png",
725 | 				email_verified: true,
726 | 			};
727 | 			userInfoResponse.statusCode = 200;
728 | 		});
729 | 
730 | 		const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
731 | 			plugins: [
732 | 				genericOAuth({
733 | 					config: [
734 | 						{
735 | 							providerId: "map-profile-numeric",
736 | 							discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
737 | 							clientId: clientId,
738 | 							clientSecret: clientSecret,
739 | 							pkce: true,
740 | 							mapProfileToUser: (profile) => {
741 | 								return {
742 | 									id: profile.user_id,
743 | 									email: profile.email,
744 | 									name: profile.name,
745 | 									emailVerified: profile.email_verified,
746 | 								};
747 | 							},
748 | 						},
749 | 					],
750 | 				}),
751 | 			],
752 | 		});
753 | 		const headers = new Headers();
754 | 		const authClient = createAuthClient({
755 | 			plugins: [genericOAuthClient()],
756 | 			baseURL: "http://localhost:3000",
757 | 			fetchOptions: {
758 | 				customFetchImpl,
759 | 				onSuccess: cookieSetter(headers),
760 | 			},
761 | 		});
762 | 
763 | 		const signInRes = await authClient.signIn.oauth2({
764 | 			providerId: "map-profile-numeric",
765 | 			callbackURL: "http://localhost:3000/dashboard",
766 | 			newUserCallbackURL: "http://localhost:3000/new_user",
767 | 		});
768 | 
769 | 		const { callbackURL, headers: newHeaders } = await simulateOAuthFlow(
770 | 			signInRes.data?.url || "",
771 | 			headers,
772 | 			customFetchImpl,
773 | 		);
774 | 
775 | 		expect(callbackURL).toBe("http://localhost:3000/new_user");
776 | 
777 | 		const session = await authClient.getSession({
778 | 			fetchOptions: {
779 | 				headers: newHeaders,
780 | 			},
781 | 		});
782 | 
783 | 		const ctx = await auth.$context;
784 | 		const accounts = await ctx.internalAdapter.findAccounts(
785 | 			session.data?.user.id!,
786 | 		);
787 | 
788 | 		expect(accounts[0]!.accountId).toBe(String(numericProfileId));
789 | 	});
790 | 
791 | 	it("should handle Strava OAuth with custom mapProfileToUser", async () => {
792 | 		const stravaUserId = 12345678;
793 | 		const stravaProfile = {
794 | 			id: stravaUserId,
795 | 			firstname: "John",
796 | 			lastname: "Doe",
797 | 			profile: "https://example.com/strava-avatar.jpg",
798 | 			email_verified: true,
799 | 		};
800 | 
801 | 		server.service.once("beforeUserinfo", (userInfoResponse) => {
802 | 			userInfoResponse.body = stravaProfile;
803 | 			userInfoResponse.statusCode = 200;
804 | 		});
805 | 
806 | 		const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
807 | 			plugins: [
808 | 				genericOAuth({
809 | 					config: [
810 | 						{
811 | 							providerId: "strava",
812 | 							authorizationUrl: `http://localhost:${port}/authorize`,
813 | 							tokenUrl: `http://localhost:${port}/token`,
814 | 							userInfoUrl: `http://localhost:${port}/userinfo`,
815 | 							clientId: "STRAVA_CLIENT_ID",
816 | 							clientSecret: "STRAVA_CLIENT_SECRET",
817 | 							scopes: ["read", "activity:read_all"],
818 | 							pkce: true,
819 | 							mapProfileToUser: (profile) => {
820 | 								const fullName = `${profile.firstname} ${profile.lastname}`;
821 | 								return {
822 | 									id: profile.id,
823 | 									email: `${profile.id}@strava.local`,
824 | 									name: fullName,
825 | 									image: profile.profile,
826 | 									emailVerified: true,
827 | 								};
828 | 							},
829 | 						},
830 | 					],
831 | 				}),
832 | 			],
833 | 		});
834 | 		const headers = new Headers();
835 | 		const authClient = createAuthClient({
836 | 			plugins: [genericOAuthClient()],
837 | 			baseURL: "http://localhost:3000",
838 | 			fetchOptions: {
839 | 				customFetchImpl,
840 | 				onSuccess: cookieSetter(headers),
841 | 			},
842 | 		});
843 | 
844 | 		const signInRes = await authClient.signIn.oauth2({
845 | 			providerId: "strava",
846 | 			callbackURL: "http://localhost:3000/dashboard",
847 | 			newUserCallbackURL: "http://localhost:3000/new_user",
848 | 		});
849 | 
850 | 		expect(signInRes.data?.url).toContain(`http://localhost:${port}/authorize`);
851 | 		// we missed the `activity:read_all`
852 | 		expect(signInRes.data?.url).toContain("scope=read+activity");
853 | 
854 | 		const { callbackURL, headers: newHeaders } = await simulateOAuthFlow(
855 | 			signInRes.data?.url || "",
856 | 			headers,
857 | 			customFetchImpl,
858 | 		);
859 | 
860 | 		expect(callbackURL).toBe("http://localhost:3000/new_user");
861 | 
862 | 		const session = await authClient.getSession({
863 | 			fetchOptions: {
864 | 				headers: newHeaders,
865 | 			},
866 | 		});
867 | 
868 | 		expect(session.data).not.toBeNull();
869 | 		expect(session.data?.user.email).toBe(`${stravaUserId}@strava.local`);
870 | 		expect(session.data?.user.name).toBe("John Doe");
871 | 		expect(session.data?.user.image).toBe(
872 | 			"https://example.com/strava-avatar.jpg",
873 | 		);
874 | 
875 | 		const ctx = await auth.$context;
876 | 		const accounts = await ctx.internalAdapter.findAccounts(
877 | 			session.data?.user.id!,
878 | 		);
879 | 
880 | 		expect(accounts[0]).toMatchObject({
881 | 			providerId: "strava",
882 | 			accountId: String(stravaUserId),
883 | 			userId: session.data?.user.id,
884 | 		});
885 | 	});
886 | });
887 | 
```
Page 45/67FirstPrevNextLast