This is page 44 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/adapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { Session, User } from "../../types"; 2 | import { getDate } from "../../utils/date"; 3 | import type { OrganizationOptions } from "./types"; 4 | import type { 5 | InferInvitation, 6 | InferMember, 7 | InferOrganization, 8 | InferTeam, 9 | InvitationInput, 10 | Member, 11 | MemberInput, 12 | OrganizationInput, 13 | Team, 14 | TeamInput, 15 | TeamMember, 16 | } from "./schema"; 17 | import { BetterAuthError } from "@better-auth/core/error"; 18 | import parseJSON from "../../client/parser"; 19 | import { type InferAdditionalFieldsFromPluginOptions } from "../../db"; 20 | import { getCurrentAdapter } from "@better-auth/core/context"; 21 | import type { AuthContext, GenericEndpointContext } from "@better-auth/core"; 22 | 23 | export const getOrgAdapter = <O extends OrganizationOptions>( 24 | context: AuthContext, 25 | options?: O, 26 | ) => { 27 | const baseAdapter = context.adapter; 28 | return { 29 | findOrganizationBySlug: async (slug: string) => { 30 | const adapter = await getCurrentAdapter(baseAdapter); 31 | const organization = await adapter.findOne<InferOrganization<O>>({ 32 | model: "organization", 33 | where: [ 34 | { 35 | field: "slug", 36 | value: slug, 37 | }, 38 | ], 39 | }); 40 | return organization; 41 | }, 42 | createOrganization: async (data: { 43 | organization: OrganizationInput & 44 | // This represents the additional fields from the plugin options 45 | Record<string, any>; 46 | }) => { 47 | const adapter = await getCurrentAdapter(baseAdapter); 48 | const organization = await adapter.create< 49 | OrganizationInput, 50 | InferOrganization<O, false> 51 | >({ 52 | model: "organization", 53 | data: { 54 | ...data.organization, 55 | metadata: data.organization.metadata 56 | ? JSON.stringify(data.organization.metadata) 57 | : undefined, 58 | }, 59 | forceAllowId: true, 60 | }); 61 | 62 | return { 63 | ...organization, 64 | metadata: 65 | organization.metadata && typeof organization.metadata === "string" 66 | ? JSON.parse(organization.metadata) 67 | : undefined, 68 | } as typeof organization; 69 | }, 70 | findMemberByEmail: async (data: { 71 | email: string; 72 | organizationId: string; 73 | }) => { 74 | const adapter = await getCurrentAdapter(baseAdapter); 75 | const user = await adapter.findOne<User>({ 76 | model: "user", 77 | where: [ 78 | { 79 | field: "email", 80 | value: data.email.toLowerCase(), 81 | }, 82 | ], 83 | }); 84 | if (!user) { 85 | return null; 86 | } 87 | const member = await adapter.findOne<Member>({ 88 | model: "member", 89 | where: [ 90 | { 91 | field: "organizationId", 92 | value: data.organizationId, 93 | }, 94 | { 95 | field: "userId", 96 | value: user.id, 97 | }, 98 | ], 99 | }); 100 | if (!member) { 101 | return null; 102 | } 103 | return { 104 | ...member, 105 | user: { 106 | id: user.id, 107 | name: user.name, 108 | email: user.email, 109 | image: user.image, 110 | }, 111 | }; 112 | }, 113 | listMembers: async (data: { 114 | organizationId?: string; 115 | limit?: number; 116 | offset?: number; 117 | sortBy?: string; 118 | sortOrder?: "asc" | "desc"; 119 | filter?: { 120 | field: string; 121 | operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "contains"; 122 | value: any; 123 | }; 124 | }) => { 125 | const adapter = await getCurrentAdapter(baseAdapter); 126 | const members = await Promise.all([ 127 | adapter.findMany<Member>({ 128 | model: "member", 129 | where: [ 130 | { field: "organizationId", value: data.organizationId }, 131 | ...(data.filter?.field 132 | ? [ 133 | { 134 | field: data.filter?.field, 135 | value: data.filter?.value, 136 | }, 137 | ] 138 | : []), 139 | ], 140 | limit: data.limit || options?.membershipLimit || 100, 141 | offset: data.offset || 0, 142 | sortBy: data.sortBy 143 | ? { field: data.sortBy, direction: data.sortOrder || "asc" } 144 | : undefined, 145 | }), 146 | adapter.count({ 147 | model: "member", 148 | where: [ 149 | { field: "organizationId", value: data.organizationId }, 150 | ...(data.filter?.field 151 | ? [ 152 | { 153 | field: data.filter?.field, 154 | value: data.filter?.value, 155 | }, 156 | ] 157 | : []), 158 | ], 159 | }), 160 | ]); 161 | const users = await adapter.findMany<User>({ 162 | model: "user", 163 | where: [ 164 | { 165 | field: "id", 166 | value: members[0].map((member) => member.userId), 167 | operator: "in", 168 | }, 169 | ], 170 | }); 171 | return { 172 | members: members[0].map((member) => { 173 | const user = users.find((user) => user.id === member.userId); 174 | if (!user) { 175 | throw new BetterAuthError( 176 | "Unexpected error: User not found for member", 177 | ); 178 | } 179 | return { 180 | ...member, 181 | user: { 182 | id: user.id, 183 | name: user.name, 184 | email: user.email, 185 | image: user.image, 186 | }, 187 | }; 188 | }), 189 | total: members[1], 190 | }; 191 | }, 192 | findMemberByOrgId: async (data: { 193 | userId: string; 194 | organizationId: string; 195 | }) => { 196 | const adapter = await getCurrentAdapter(baseAdapter); 197 | const [member, user] = await Promise.all([ 198 | await adapter.findOne<Member>({ 199 | model: "member", 200 | where: [ 201 | { 202 | field: "userId", 203 | value: data.userId, 204 | }, 205 | { 206 | field: "organizationId", 207 | value: data.organizationId, 208 | }, 209 | ], 210 | }), 211 | await adapter.findOne<User>({ 212 | model: "user", 213 | where: [ 214 | { 215 | field: "id", 216 | value: data.userId, 217 | }, 218 | ], 219 | }), 220 | ]); 221 | if (!user || !member) { 222 | return null; 223 | } 224 | return { 225 | ...member, 226 | user: { 227 | id: user.id, 228 | name: user.name, 229 | email: user.email, 230 | image: user.image, 231 | }, 232 | }; 233 | }, 234 | findMemberById: async (memberId: string) => { 235 | const adapter = await getCurrentAdapter(baseAdapter); 236 | const member = await adapter.findOne<Member>({ 237 | model: "member", 238 | where: [ 239 | { 240 | field: "id", 241 | value: memberId, 242 | }, 243 | ], 244 | }); 245 | if (!member) { 246 | return null; 247 | } 248 | const user = await adapter.findOne<User>({ 249 | model: "user", 250 | where: [ 251 | { 252 | field: "id", 253 | value: member.userId, 254 | }, 255 | ], 256 | }); 257 | if (!user) { 258 | return null; 259 | } 260 | return { 261 | ...member, 262 | user: { 263 | id: user.id, 264 | name: user.name, 265 | email: user.email, 266 | image: user.image, 267 | }, 268 | }; 269 | }, 270 | createMember: async ( 271 | data: Omit<MemberInput, "id"> & 272 | // Additional fields from the plugin options 273 | Record<string, any>, 274 | ) => { 275 | const adapter = await getCurrentAdapter(baseAdapter); 276 | const member = await adapter.create< 277 | typeof data, 278 | Member & InferAdditionalFieldsFromPluginOptions<"member", O, false> 279 | >({ 280 | model: "member", 281 | data: { 282 | ...data, 283 | createdAt: new Date(), 284 | }, 285 | }); 286 | return member; 287 | }, 288 | updateMember: async (memberId: string, role: string) => { 289 | const adapter = await getCurrentAdapter(baseAdapter); 290 | const member = await adapter.update<InferMember<O>>({ 291 | model: "member", 292 | where: [ 293 | { 294 | field: "id", 295 | value: memberId, 296 | }, 297 | ], 298 | update: { 299 | role, 300 | }, 301 | }); 302 | return member; 303 | }, 304 | deleteMember: async (memberId: string) => { 305 | const adapter = await getCurrentAdapter(baseAdapter); 306 | const member = await adapter.delete<InferMember<O>>({ 307 | model: "member", 308 | where: [ 309 | { 310 | field: "id", 311 | value: memberId, 312 | }, 313 | ], 314 | }); 315 | return member; 316 | }, 317 | updateOrganization: async ( 318 | organizationId: string, 319 | data: Partial<OrganizationInput>, 320 | ) => { 321 | const adapter = await getCurrentAdapter(baseAdapter); 322 | const organization = await adapter.update<InferOrganization<O>>({ 323 | model: "organization", 324 | where: [ 325 | { 326 | field: "id", 327 | value: organizationId, 328 | }, 329 | ], 330 | update: { 331 | ...data, 332 | metadata: 333 | typeof data.metadata === "object" 334 | ? JSON.stringify(data.metadata) 335 | : data.metadata, 336 | }, 337 | }); 338 | if (!organization) { 339 | return null; 340 | } 341 | return { 342 | ...organization, 343 | metadata: organization.metadata 344 | ? parseJSON<Record<string, any>>(organization.metadata) 345 | : undefined, 346 | }; 347 | }, 348 | deleteOrganization: async (organizationId: string) => { 349 | const adapter = await getCurrentAdapter(baseAdapter); 350 | await adapter.delete({ 351 | model: "member", 352 | where: [ 353 | { 354 | field: "organizationId", 355 | value: organizationId, 356 | }, 357 | ], 358 | }); 359 | await adapter.delete({ 360 | model: "invitation", 361 | where: [ 362 | { 363 | field: "organizationId", 364 | value: organizationId, 365 | }, 366 | ], 367 | }); 368 | await adapter.delete<InferOrganization<O>>({ 369 | model: "organization", 370 | where: [ 371 | { 372 | field: "id", 373 | value: organizationId, 374 | }, 375 | ], 376 | }); 377 | return organizationId; 378 | }, 379 | setActiveOrganization: async ( 380 | sessionToken: string, 381 | organizationId: string | null, 382 | ctx: GenericEndpointContext, 383 | ) => { 384 | const session = await context.internalAdapter.updateSession( 385 | sessionToken, 386 | { 387 | activeOrganizationId: organizationId, 388 | }, 389 | ctx, 390 | ); 391 | return session as Session; 392 | }, 393 | findOrganizationById: async (organizationId: string) => { 394 | const adapter = await getCurrentAdapter(baseAdapter); 395 | const organization = await adapter.findOne<InferOrganization<O>>({ 396 | model: "organization", 397 | where: [ 398 | { 399 | field: "id", 400 | value: organizationId, 401 | }, 402 | ], 403 | }); 404 | return organization; 405 | }, 406 | checkMembership: async ({ 407 | userId, 408 | organizationId, 409 | }: { 410 | userId: string; 411 | organizationId: string; 412 | }) => { 413 | const adapter = await getCurrentAdapter(baseAdapter); 414 | const member = await adapter.findOne<InferMember<O>>({ 415 | model: "member", 416 | where: [ 417 | { 418 | field: "userId", 419 | value: userId, 420 | }, 421 | { 422 | field: "organizationId", 423 | value: organizationId, 424 | }, 425 | ], 426 | }); 427 | return member; 428 | }, 429 | /** 430 | * @requires db 431 | */ 432 | findFullOrganization: async ({ 433 | organizationId, 434 | isSlug, 435 | includeTeams, 436 | membersLimit, 437 | }: { 438 | organizationId: string; 439 | isSlug?: boolean; 440 | includeTeams?: boolean; 441 | membersLimit?: number; 442 | }) => { 443 | const adapter = await getCurrentAdapter(baseAdapter); 444 | const org = await adapter.findOne<InferOrganization<O>>({ 445 | model: "organization", 446 | where: [{ field: isSlug ? "slug" : "id", value: organizationId }], 447 | }); 448 | if (!org) { 449 | return null; 450 | } 451 | const [invitations, members, teams] = await Promise.all([ 452 | adapter.findMany<InferInvitation<O>>({ 453 | model: "invitation", 454 | where: [{ field: "organizationId", value: org.id }], 455 | }), 456 | adapter.findMany<InferMember<O>>({ 457 | model: "member", 458 | where: [{ field: "organizationId", value: org.id }], 459 | limit: membersLimit ?? options?.membershipLimit ?? 100, 460 | }), 461 | includeTeams 462 | ? adapter.findMany<InferTeam<O>>({ 463 | model: "team", 464 | where: [{ field: "organizationId", value: org.id }], 465 | }) 466 | : null, 467 | ]); 468 | 469 | if (!org) return null; 470 | 471 | const userIds = members.map((member) => member.userId); 472 | const users = 473 | userIds.length > 0 474 | ? await adapter.findMany<User>({ 475 | model: "user", 476 | where: [{ field: "id", value: userIds, operator: "in" }], 477 | limit: options?.membershipLimit || 100, 478 | }) 479 | : []; 480 | 481 | const userMap = new Map(users.map((user) => [user.id, user])); 482 | const membersWithUsers = members.map((member) => { 483 | const user = userMap.get(member.userId); 484 | if (!user) { 485 | throw new BetterAuthError( 486 | "Unexpected error: User not found for member", 487 | ); 488 | } 489 | return { 490 | ...member, 491 | user: { 492 | id: user.id, 493 | name: user.name, 494 | email: user.email, 495 | image: user.image, 496 | }, 497 | }; 498 | }); 499 | 500 | return { 501 | ...org, 502 | invitations, 503 | members: membersWithUsers, 504 | teams, 505 | }; 506 | }, 507 | listOrganizations: async (userId: string) => { 508 | const adapter = await getCurrentAdapter(baseAdapter); 509 | const members = await adapter.findMany<InferMember<O>>({ 510 | model: "member", 511 | where: [ 512 | { 513 | field: "userId", 514 | value: userId, 515 | }, 516 | ], 517 | }); 518 | 519 | if (!members || members.length === 0) { 520 | return []; 521 | } 522 | 523 | const organizationIds = members.map((member) => member.organizationId); 524 | 525 | const organizations = await adapter.findMany<InferOrganization<O>>({ 526 | model: "organization", 527 | where: [ 528 | { 529 | field: "id", 530 | value: organizationIds, 531 | operator: "in", 532 | }, 533 | ], 534 | }); 535 | return organizations; 536 | }, 537 | createTeam: async (data: Omit<TeamInput, "id">) => { 538 | const adapter = await getCurrentAdapter(baseAdapter); 539 | const team = await adapter.create<Omit<TeamInput, "id">, InferTeam<O>>({ 540 | model: "team", 541 | data, 542 | }); 543 | return team; 544 | }, 545 | findTeamById: async <IncludeMembers extends boolean>({ 546 | teamId, 547 | organizationId, 548 | includeTeamMembers, 549 | }: { 550 | teamId: string; 551 | organizationId?: string; 552 | includeTeamMembers?: IncludeMembers; 553 | }): Promise< 554 | | (InferTeam<O> & 555 | (IncludeMembers extends true ? { members: TeamMember[] } : {})) 556 | | null 557 | > => { 558 | const adapter = await getCurrentAdapter(baseAdapter); 559 | const team = await adapter.findOne<InferTeam<O>>({ 560 | model: "team", 561 | where: [ 562 | { 563 | field: "id", 564 | value: teamId, 565 | }, 566 | ...(organizationId 567 | ? [ 568 | { 569 | field: "organizationId", 570 | value: organizationId, 571 | }, 572 | ] 573 | : []), 574 | ], 575 | }); 576 | if (!team) { 577 | return null; 578 | } 579 | 580 | let members: TeamMember[] = []; 581 | if (includeTeamMembers) { 582 | members = await adapter.findMany<TeamMember>({ 583 | model: "teamMember", 584 | where: [ 585 | { 586 | field: "teamId", 587 | value: teamId, 588 | }, 589 | ], 590 | limit: options?.membershipLimit || 100, 591 | }); 592 | return { 593 | ...team, 594 | members, 595 | }; 596 | } 597 | 598 | return team as InferTeam<O> & 599 | (IncludeMembers extends true ? { members: TeamMember[] } : {}); 600 | }, 601 | updateTeam: async ( 602 | teamId: string, 603 | data: { name?: string; description?: string; status?: string }, 604 | ) => { 605 | const adapter = await getCurrentAdapter(baseAdapter); 606 | if ("id" in data) data.id = undefined; 607 | const team = await adapter.update< 608 | Team & InferAdditionalFieldsFromPluginOptions<"team", O> 609 | >({ 610 | model: "team", 611 | where: [ 612 | { 613 | field: "id", 614 | value: teamId, 615 | }, 616 | ], 617 | update: { 618 | ...data, 619 | }, 620 | }); 621 | return team; 622 | }, 623 | 624 | deleteTeam: async (teamId: string) => { 625 | const adapter = await getCurrentAdapter(baseAdapter); 626 | await adapter.deleteMany({ 627 | model: "teamMember", 628 | where: [ 629 | { 630 | field: "teamId", 631 | value: teamId, 632 | }, 633 | ], 634 | }); 635 | const team = await adapter.delete<Team>({ 636 | model: "team", 637 | where: [ 638 | { 639 | field: "id", 640 | value: teamId, 641 | }, 642 | ], 643 | }); 644 | return team; 645 | }, 646 | 647 | listTeams: async (organizationId: string) => { 648 | const adapter = await getCurrentAdapter(baseAdapter); 649 | const teams = await adapter.findMany<Team>({ 650 | model: "team", 651 | where: [ 652 | { 653 | field: "organizationId", 654 | value: organizationId, 655 | }, 656 | ], 657 | }); 658 | return teams; 659 | }, 660 | 661 | createTeamInvitation: async ({ 662 | email, 663 | role, 664 | teamId, 665 | organizationId, 666 | inviterId, 667 | expiresIn = 1000 * 60 * 60 * 48, // Default expiration: 48 hours 668 | }: { 669 | email: string; 670 | role: string; 671 | teamId: string; 672 | organizationId: string; 673 | inviterId: string; 674 | expiresIn?: number; 675 | }) => { 676 | const adapter = await getCurrentAdapter(baseAdapter); 677 | const expiresAt = getDate(expiresIn); // Get expiration date 678 | 679 | const invitation = await adapter.create< 680 | InvitationInput, 681 | InferInvitation<O> 682 | >({ 683 | model: "invitation", 684 | data: { 685 | email, 686 | role, 687 | organizationId, 688 | teamId, 689 | inviterId, 690 | status: "pending", 691 | expiresAt, 692 | }, 693 | }); 694 | 695 | return invitation; 696 | }, 697 | 698 | setActiveTeam: async ( 699 | sessionToken: string, 700 | teamId: string | null, 701 | ctx: GenericEndpointContext, 702 | ) => { 703 | const session = await context.internalAdapter.updateSession( 704 | sessionToken, 705 | { 706 | activeTeamId: teamId, 707 | }, 708 | ctx, 709 | ); 710 | return session as Session; 711 | }, 712 | 713 | listTeamMembers: async (data: { teamId: string }) => { 714 | const adapter = await getCurrentAdapter(baseAdapter); 715 | const members = await adapter.findMany<TeamMember>({ 716 | model: "teamMember", 717 | where: [ 718 | { 719 | field: "teamId", 720 | value: data.teamId, 721 | }, 722 | ], 723 | }); 724 | 725 | return members; 726 | }, 727 | countTeamMembers: async (data: { teamId: string }) => { 728 | const adapter = await getCurrentAdapter(baseAdapter); 729 | const count = await adapter.count({ 730 | model: "teamMember", 731 | where: [{ field: "teamId", value: data.teamId }], 732 | }); 733 | return count; 734 | }, 735 | countMembers: async (data: { organizationId: string }) => { 736 | const adapter = await getCurrentAdapter(baseAdapter); 737 | const count = await adapter.count({ 738 | model: "member", 739 | where: [{ field: "organizationId", value: data.organizationId }], 740 | }); 741 | return count; 742 | }, 743 | listTeamsByUser: async (data: { userId: string }) => { 744 | const adapter = await getCurrentAdapter(baseAdapter); 745 | const members = await adapter.findMany<TeamMember>({ 746 | model: "teamMember", 747 | where: [ 748 | { 749 | field: "userId", 750 | value: data.userId, 751 | }, 752 | ], 753 | }); 754 | 755 | const teams = await adapter.findMany<Team>({ 756 | model: "team", 757 | where: [ 758 | { 759 | field: "id", 760 | operator: "in", 761 | value: members.map((m) => m.teamId), 762 | }, 763 | ], 764 | }); 765 | 766 | return teams; 767 | }, 768 | 769 | findTeamMember: async (data: { teamId: string; userId: string }) => { 770 | const adapter = await getCurrentAdapter(baseAdapter); 771 | const member = await adapter.findOne<TeamMember>({ 772 | model: "teamMember", 773 | where: [ 774 | { 775 | field: "teamId", 776 | value: data.teamId, 777 | }, 778 | { 779 | field: "userId", 780 | value: data.userId, 781 | }, 782 | ], 783 | }); 784 | 785 | return member; 786 | }, 787 | 788 | findOrCreateTeamMember: async (data: { 789 | teamId: string; 790 | userId: string; 791 | }) => { 792 | const adapter = await getCurrentAdapter(baseAdapter); 793 | const member = await adapter.findOne<TeamMember>({ 794 | model: "teamMember", 795 | where: [ 796 | { 797 | field: "teamId", 798 | value: data.teamId, 799 | }, 800 | { 801 | field: "userId", 802 | value: data.userId, 803 | }, 804 | ], 805 | }); 806 | 807 | if (member) return member; 808 | 809 | return await adapter.create<Omit<TeamMember, "id">, TeamMember>({ 810 | model: "teamMember", 811 | data: { 812 | teamId: data.teamId, 813 | userId: data.userId, 814 | createdAt: new Date(), 815 | }, 816 | }); 817 | }, 818 | 819 | removeTeamMember: async (data: { teamId: string; userId: string }) => { 820 | const adapter = await getCurrentAdapter(baseAdapter); 821 | await adapter.delete({ 822 | model: "teamMember", 823 | where: [ 824 | { 825 | field: "teamId", 826 | value: data.teamId, 827 | }, 828 | { 829 | field: "userId", 830 | value: data.userId, 831 | }, 832 | ], 833 | }); 834 | }, 835 | 836 | findInvitationsByTeamId: async (teamId: string) => { 837 | const adapter = await getCurrentAdapter(baseAdapter); 838 | const invitations = await adapter.findMany<InferInvitation<O>>({ 839 | model: "invitation", 840 | where: [ 841 | { 842 | field: "teamId", 843 | value: teamId, 844 | }, 845 | ], 846 | }); 847 | return invitations; 848 | }, 849 | listUserInvitations: async (email: string) => { 850 | const adapter = await getCurrentAdapter(baseAdapter); 851 | const invitations = await adapter.findMany<InferInvitation<O>>({ 852 | model: "invitation", 853 | where: [{ field: "email", value: email.toLowerCase() }], 854 | }); 855 | return invitations; 856 | }, 857 | createInvitation: async ({ 858 | invitation, 859 | user, 860 | }: { 861 | invitation: { 862 | email: string; 863 | role: string; 864 | organizationId: string; 865 | teamIds: string[]; 866 | } & Record<string, any>; // This represents the additionalFields for the invitation 867 | user: User; 868 | }) => { 869 | const adapter = await getCurrentAdapter(baseAdapter); 870 | const defaultExpiration = 60 * 60 * 48; 871 | const expiresAt = getDate( 872 | options?.invitationExpiresIn || defaultExpiration, 873 | "sec", 874 | ); 875 | const invite = await adapter.create< 876 | Omit<InvitationInput, "id">, 877 | InferInvitation<O> 878 | >({ 879 | model: "invitation", 880 | data: { 881 | status: "pending", 882 | expiresAt, 883 | createdAt: new Date(), 884 | inviterId: user.id, 885 | ...invitation, 886 | teamId: 887 | invitation.teamIds.length > 0 ? invitation.teamIds.join(",") : null, 888 | }, 889 | }); 890 | 891 | return invite; 892 | }, 893 | findInvitationById: async (id: string) => { 894 | const adapter = await getCurrentAdapter(baseAdapter); 895 | const invitation = await adapter.findOne<InferInvitation<O>>({ 896 | model: "invitation", 897 | where: [ 898 | { 899 | field: "id", 900 | value: id, 901 | }, 902 | ], 903 | }); 904 | return invitation; 905 | }, 906 | findPendingInvitation: async (data: { 907 | email: string; 908 | organizationId: string; 909 | }) => { 910 | const adapter = await getCurrentAdapter(baseAdapter); 911 | const invitation = await adapter.findMany<InferInvitation<O>>({ 912 | model: "invitation", 913 | where: [ 914 | { 915 | field: "email", 916 | value: data.email.toLowerCase(), 917 | }, 918 | { 919 | field: "organizationId", 920 | value: data.organizationId, 921 | }, 922 | { 923 | field: "status", 924 | value: "pending", 925 | }, 926 | ], 927 | }); 928 | return invitation.filter( 929 | (invite) => new Date(invite.expiresAt) > new Date(), 930 | ); 931 | }, 932 | findPendingInvitations: async (data: { organizationId: string }) => { 933 | const adapter = await getCurrentAdapter(baseAdapter); 934 | const invitations = await adapter.findMany<InferInvitation<O>>({ 935 | model: "invitation", 936 | where: [ 937 | { 938 | field: "organizationId", 939 | value: data.organizationId, 940 | }, 941 | { 942 | field: "status", 943 | value: "pending", 944 | }, 945 | ], 946 | }); 947 | return invitations.filter( 948 | (invite) => new Date(invite.expiresAt) > new Date(), 949 | ); 950 | }, 951 | listInvitations: async (data: { organizationId: string }) => { 952 | const adapter = await getCurrentAdapter(baseAdapter); 953 | const invitations = await adapter.findMany<InferInvitation<O>>({ 954 | model: "invitation", 955 | where: [ 956 | { 957 | field: "organizationId", 958 | value: data.organizationId, 959 | }, 960 | ], 961 | }); 962 | return invitations; 963 | }, 964 | updateInvitation: async (data: { 965 | invitationId: string; 966 | status: "accepted" | "canceled" | "rejected"; 967 | }) => { 968 | const adapter = await getCurrentAdapter(baseAdapter); 969 | const invitation = await adapter.update<InferInvitation<O>>({ 970 | model: "invitation", 971 | where: [ 972 | { 973 | field: "id", 974 | value: data.invitationId, 975 | }, 976 | ], 977 | update: { 978 | status: data.status, 979 | }, 980 | }); 981 | return invitation; 982 | }, 983 | }; 984 | }; 985 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/oidc-provider/oidc.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | afterAll, 3 | afterEach, 4 | beforeAll, 5 | describe, 6 | expect, 7 | it, 8 | test, 9 | } from "vitest"; 10 | import { getTestInstance } from "../../test-utils/test-instance"; 11 | import { oidcProvider } from "."; 12 | import { genericOAuth } from "../generic-oauth"; 13 | import type { Client } from "./types"; 14 | import { createAuthClient } from "../../client"; 15 | import { oidcClient } from "./client"; 16 | import { genericOAuthClient } from "../generic-oauth/client"; 17 | import { listen, type Listener } from "listhen"; 18 | import { toNodeHandler } from "../../integrations/node"; 19 | import { jwt } from "../jwt"; 20 | import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose"; 21 | 22 | // Type for the server client with OIDC plugin 23 | type ServerClient = ReturnType< 24 | typeof createAuthClient<{ 25 | plugins: [ReturnType<typeof oidcClient>]; 26 | }> 27 | >; 28 | 29 | /** 30 | * Helper to handle OIDC consent flow when required per OIDC spec 31 | */ 32 | async function handleConsentFlow( 33 | redirectURI: string, 34 | serverClient: ServerClient, 35 | sessionHeaders: Headers, 36 | consentHeaders: Headers, 37 | ): Promise<string> { 38 | if (!redirectURI.includes("consent_code=")) { 39 | return redirectURI; 40 | } 41 | 42 | // Extract consent code from redirect URL 43 | const url = new URL(redirectURI, "http://localhost:3000"); 44 | const consentCode = url.searchParams.get("consent_code"); 45 | 46 | if (!consentCode) { 47 | throw new Error("Consent code not found in redirect URL"); 48 | } 49 | 50 | // Merge session headers with consent cookies 51 | const authHeaders = new Headers(sessionHeaders); 52 | consentHeaders.forEach((value, key) => { 53 | if (key.toLowerCase() === "cookie") { 54 | const existing = authHeaders.get("Cookie") || ""; 55 | authHeaders.set("Cookie", existing ? `${existing}; ${value}` : value); 56 | } else { 57 | authHeaders.set(key, value); 58 | } 59 | }); 60 | 61 | // Accept consent 62 | const response = await serverClient.oauth2.consent( 63 | { accept: true, consent_code: consentCode }, 64 | { headers: authHeaders, throw: true }, 65 | ); 66 | 67 | return response.redirectURI; 68 | } 69 | 70 | describe("oidc", async () => { 71 | const { 72 | auth: authorizationServer, 73 | signInWithTestUser, 74 | customFetchImpl, 75 | testUser, 76 | } = await getTestInstance({ 77 | baseURL: "http://localhost:3000", 78 | plugins: [ 79 | oidcProvider({ 80 | loginPage: "/login", 81 | consentPage: "/oauth2/authorize", 82 | requirePKCE: true, 83 | getAdditionalUserInfoClaim(user) { 84 | return { 85 | custom: "custom value", 86 | userId: user.id, 87 | }; 88 | }, 89 | }), 90 | jwt(), 91 | ], 92 | }); 93 | const { headers } = await signInWithTestUser(); 94 | const serverClient = createAuthClient({ 95 | plugins: [oidcClient()], 96 | baseURL: "http://localhost:3000", 97 | fetchOptions: { 98 | customFetchImpl, 99 | headers, 100 | }, 101 | }); 102 | 103 | let server: Listener; 104 | 105 | beforeAll(async () => { 106 | server = await listen(toNodeHandler(authorizationServer.handler), { 107 | port: 3000, 108 | }); 109 | }); 110 | 111 | afterAll(async () => { 112 | await server.close(); 113 | }); 114 | 115 | let application: Client = { 116 | clientId: "test-client-id", 117 | clientSecret: "test-client-secret-oidc", 118 | redirectURLs: ["http://localhost:3000/api/auth/oauth2/callback/test"], 119 | metadata: {}, 120 | icon: "", 121 | type: "web", 122 | disabled: false, 123 | name: "test", 124 | }; 125 | 126 | it("should create oidc client", async ({ expect }) => { 127 | const createdClient = await serverClient.oauth2.register({ 128 | client_name: application.name, 129 | redirect_uris: application.redirectURLs, 130 | logo_uri: application.icon, 131 | }); 132 | expect(createdClient.data).toMatchObject({ 133 | client_id: expect.any(String), 134 | client_secret: expect.any(String), 135 | client_name: "test", 136 | logo_uri: "", 137 | redirect_uris: ["http://localhost:3000/api/auth/oauth2/callback/test"], 138 | grant_types: ["authorization_code"], 139 | response_types: ["code"], 140 | token_endpoint_auth_method: "client_secret_basic", 141 | client_id_issued_at: expect.any(Number), 142 | client_secret_expires_at: 0, 143 | }); 144 | if (createdClient.data) { 145 | application = { 146 | clientId: createdClient.data.client_id, 147 | clientSecret: createdClient.data.client_secret, 148 | redirectURLs: createdClient.data.redirect_uris, 149 | metadata: {}, 150 | icon: createdClient.data.logo_uri || "", 151 | type: "web", 152 | disabled: false, 153 | name: createdClient.data.client_name || "", 154 | }; 155 | } 156 | }); 157 | 158 | it("should sign in the user with the provider", async ({ expect }) => { 159 | // The RP (Relying Party) - the client application 160 | const { customFetchImpl: customFetchImplRP, cookieSetter } = 161 | await getTestInstance({ 162 | account: { 163 | accountLinking: { 164 | trustedProviders: ["test"], 165 | }, 166 | }, 167 | plugins: [ 168 | genericOAuth({ 169 | config: [ 170 | { 171 | providerId: "test", 172 | clientId: application.clientId, 173 | clientSecret: application.clientSecret || "", 174 | authorizationUrl: 175 | "http://localhost:3000/api/auth/oauth2/authorize", 176 | tokenUrl: "http://localhost:3000/api/auth/oauth2/token", 177 | scopes: ["openid", "profile", "email"], 178 | pkce: true, 179 | }, 180 | ], 181 | }), 182 | ], 183 | }); 184 | 185 | const client = createAuthClient({ 186 | plugins: [genericOAuthClient()], 187 | baseURL: "http://localhost:5000", 188 | fetchOptions: { 189 | customFetchImpl: customFetchImplRP, 190 | }, 191 | }); 192 | const oAuthHeaders = new Headers(); 193 | const data = await client.signIn.oauth2( 194 | { 195 | providerId: "test", 196 | callbackURL: "/dashboard", 197 | }, 198 | { 199 | throw: true, 200 | onSuccess: cookieSetter(oAuthHeaders), 201 | }, 202 | ); 203 | expect(data.url).toContain( 204 | "http://localhost:3000/api/auth/oauth2/authorize", 205 | ); 206 | expect(data.url).toContain(`client_id=${application.clientId}`); 207 | 208 | // Make the authorization request 209 | let redirectURI = ""; 210 | const consentHeaders = new Headers(); 211 | await serverClient.$fetch(data.url, { 212 | method: "GET", 213 | onError(context) { 214 | redirectURI = context.response.headers.get("Location") || ""; 215 | // Capture any consent cookies 216 | cookieSetter(consentHeaders)(context); 217 | }, 218 | }); 219 | 220 | // Handle consent flow if required (per OIDC spec for non-trusted clients) 221 | redirectURI = await handleConsentFlow( 222 | redirectURI, 223 | serverClient, 224 | headers, 225 | consentHeaders, 226 | ); 227 | 228 | // Verify we got an authorization code 229 | expect(redirectURI).toContain( 230 | "http://localhost:3000/api/auth/oauth2/callback/test?code=", 231 | ); 232 | 233 | // Complete the OAuth flow 234 | let callbackURL = ""; 235 | await client.$fetch(redirectURI, { 236 | headers: oAuthHeaders, 237 | onError(context) { 238 | callbackURL = context.response.headers.get("Location") || ""; 239 | }, 240 | }); 241 | expect(callbackURL).toContain("/dashboard"); 242 | }); 243 | 244 | it("should sign in after a consent flow", async ({ expect }) => { 245 | // The RP (Relying Party) - the client application 246 | const { customFetchImpl: customFetchImplRP, cookieSetter } = 247 | await getTestInstance({ 248 | account: { 249 | accountLinking: { 250 | trustedProviders: ["test"], 251 | }, 252 | }, 253 | plugins: [ 254 | genericOAuth({ 255 | config: [ 256 | { 257 | providerId: "test", 258 | clientId: application.clientId, 259 | clientSecret: application.clientSecret || "", 260 | authorizationUrl: 261 | "http://localhost:3000/api/auth/oauth2/authorize", 262 | tokenUrl: "http://localhost:3000/api/auth/oauth2/token", 263 | scopes: ["openid", "profile", "email"], 264 | prompt: "consent", 265 | pkce: true, 266 | }, 267 | ], 268 | }), 269 | ], 270 | }); 271 | 272 | const client = createAuthClient({ 273 | plugins: [genericOAuthClient()], 274 | baseURL: "http://localhost:5000", 275 | fetchOptions: { 276 | customFetchImpl: customFetchImplRP, 277 | }, 278 | }); 279 | const oAuthHeaders = new Headers(); 280 | const data = await client.signIn.oauth2( 281 | { 282 | providerId: "test", 283 | callbackURL: "/dashboard", 284 | }, 285 | { 286 | throw: true, 287 | onSuccess: cookieSetter(oAuthHeaders), 288 | }, 289 | ); 290 | expect(data.url).toContain( 291 | "http://localhost:3000/api/auth/oauth2/authorize", 292 | ); 293 | expect(data.url).toContain(`client_id=${application.clientId}`); 294 | 295 | let redirectURI = ""; 296 | const newHeaders = new Headers(); 297 | await serverClient.$fetch(data.url, { 298 | method: "GET", 299 | onError(context) { 300 | redirectURI = context.response.headers.get("Location") || ""; 301 | cookieSetter(newHeaders)(context); 302 | newHeaders.append("Cookie", headers.get("Cookie") || ""); 303 | }, 304 | }); 305 | expect(redirectURI).toContain("/oauth2/authorize?"); 306 | expect(redirectURI).toContain("consent_code="); 307 | expect(redirectURI).toContain("client_id="); 308 | 309 | // No need to extract consent_code - it's in the signed cookie 310 | const res = await serverClient.oauth2.consent( 311 | { 312 | accept: true, 313 | }, 314 | { 315 | headers: newHeaders, 316 | throw: true, 317 | }, 318 | ); 319 | expect(res.redirectURI).toContain( 320 | "http://localhost:3000/api/auth/oauth2/callback/test?code=", 321 | ); 322 | 323 | let callbackURL = ""; 324 | await client.$fetch(res.redirectURI, { 325 | headers: oAuthHeaders, 326 | onError(context) { 327 | callbackURL = context.response.headers.get("Location") || ""; 328 | }, 329 | }); 330 | expect(callbackURL).toContain("/dashboard"); 331 | }); 332 | 333 | it("should sign in after a login flow", async ({ expect }) => { 334 | // The RP (Relying Party) - the client application 335 | const { customFetchImpl: customFetchImplRP, cookieSetter } = 336 | await getTestInstance({ 337 | account: { 338 | accountLinking: { 339 | trustedProviders: ["test"], 340 | }, 341 | }, 342 | plugins: [ 343 | genericOAuth({ 344 | config: [ 345 | { 346 | providerId: "test", 347 | clientId: application.clientId, 348 | clientSecret: application.clientSecret || "", 349 | authorizationUrl: 350 | "http://localhost:3000/api/auth/oauth2/authorize", 351 | tokenUrl: "http://localhost:3000/api/auth/oauth2/token", 352 | scopes: ["openid", "profile", "email"], 353 | prompt: "login", 354 | pkce: true, 355 | }, 356 | ], 357 | }), 358 | ], 359 | }); 360 | 361 | const client = createAuthClient({ 362 | plugins: [genericOAuthClient()], 363 | baseURL: "http://localhost:5000", 364 | fetchOptions: { 365 | customFetchImpl: customFetchImplRP, 366 | }, 367 | }); 368 | const oAuthHeaders = new Headers(); 369 | const data = await client.signIn.oauth2( 370 | { 371 | providerId: "test", 372 | callbackURL: "/dashboard", 373 | }, 374 | { 375 | throw: true, 376 | onSuccess: cookieSetter(oAuthHeaders), 377 | }, 378 | ); 379 | expect(data.url).toContain( 380 | "http://localhost:3000/api/auth/oauth2/authorize", 381 | ); 382 | expect(data.url).toContain(`client_id=${application.clientId}`); 383 | 384 | let redirectURI = ""; 385 | const newHeaders = new Headers(); 386 | await serverClient.$fetch(data.url, { 387 | method: "GET", 388 | onError(context) { 389 | redirectURI = context.response.headers.get("Location") || ""; 390 | cookieSetter(newHeaders)(context); 391 | }, 392 | headers: newHeaders, 393 | }); 394 | expect(redirectURI).toContain("/login"); 395 | 396 | await serverClient.signIn.email( 397 | { 398 | email: testUser.email, 399 | password: testUser.password, 400 | }, 401 | { 402 | headers: newHeaders, 403 | onError(context) { 404 | redirectURI = context.response.headers.get("Location") || ""; 405 | cookieSetter(newHeaders)(context); 406 | }, 407 | }, 408 | ); 409 | 410 | expect(redirectURI).toContain( 411 | "http://localhost:3000/api/auth/oauth2/callback/test?code=", 412 | ); 413 | let callbackURL = ""; 414 | await client.$fetch(redirectURI, { 415 | headers: oAuthHeaders, 416 | onError(context) { 417 | callbackURL = context.response.headers.get("Location") || ""; 418 | }, 419 | }); 420 | expect(callbackURL).toContain("/dashboard"); 421 | }); 422 | }); 423 | 424 | describe("oidc storage", async () => { 425 | let server: Listener; 426 | 427 | afterEach(async () => { 428 | if (server) { 429 | await server.close(); 430 | } 431 | }); 432 | 433 | test.each([ 434 | { 435 | storeClientSecret: undefined, 436 | }, 437 | { 438 | storeClientSecret: "hashed", 439 | }, 440 | { 441 | storeClientSecret: "encrypted", 442 | }, 443 | ] as const)("OIDC base test", async ({ storeClientSecret }) => { 444 | const { 445 | auth: authorizationServer, 446 | signInWithTestUser, 447 | customFetchImpl, 448 | } = await getTestInstance({ 449 | baseURL: "http://localhost:3000", 450 | plugins: [ 451 | oidcProvider({ 452 | loginPage: "/login", 453 | consentPage: "/oauth2/authorize", 454 | requirePKCE: true, 455 | getAdditionalUserInfoClaim(user) { 456 | return { 457 | custom: "custom value", 458 | userId: user.id, 459 | }; 460 | }, 461 | storeClientSecret, 462 | }), 463 | jwt(), 464 | ], 465 | }); 466 | const { headers } = await signInWithTestUser(); 467 | const serverClient = createAuthClient({ 468 | plugins: [oidcClient()], 469 | baseURL: "http://localhost:3000", 470 | fetchOptions: { 471 | customFetchImpl, 472 | headers, 473 | }, 474 | }); 475 | 476 | server = await listen(toNodeHandler(authorizationServer.handler), { 477 | port: 3000, 478 | }); 479 | 480 | let application: Client = { 481 | clientId: "test-client-id", 482 | clientSecret: "test-client-secret-oidc", 483 | redirectURLs: ["http://localhost:3000/api/auth/oauth2/callback/test"], 484 | metadata: {}, 485 | icon: "", 486 | type: "web", 487 | disabled: false, 488 | name: "test", 489 | }; 490 | const createdClient = await serverClient.oauth2.register({ 491 | client_name: application.name, 492 | redirect_uris: application.redirectURLs, 493 | logo_uri: application.icon, 494 | }); 495 | expect(createdClient.data).toMatchObject({ 496 | client_id: expect.any(String), 497 | client_secret: expect.any(String), 498 | client_name: "test", 499 | logo_uri: "", 500 | redirect_uris: ["http://localhost:3000/api/auth/oauth2/callback/test"], 501 | grant_types: ["authorization_code"], 502 | response_types: ["code"], 503 | token_endpoint_auth_method: "client_secret_basic", 504 | client_id_issued_at: expect.any(Number), 505 | client_secret_expires_at: 0, 506 | }); 507 | if (createdClient.data) { 508 | application = { 509 | clientId: createdClient.data.client_id, 510 | clientSecret: createdClient.data.client_secret, 511 | redirectURLs: createdClient.data.redirect_uris, 512 | metadata: {}, 513 | icon: createdClient.data.logo_uri || "", 514 | type: "web", 515 | disabled: false, 516 | name: createdClient.data.client_name || "", 517 | }; 518 | } 519 | // The RP (Relying Party) - the client application 520 | const { customFetchImpl: customFetchImplRP, cookieSetter } = 521 | await getTestInstance({ 522 | account: { 523 | accountLinking: { 524 | trustedProviders: ["test"], 525 | }, 526 | }, 527 | plugins: [ 528 | genericOAuth({ 529 | config: [ 530 | { 531 | providerId: "test", 532 | clientId: application.clientId, 533 | clientSecret: application.clientSecret || "", 534 | authorizationUrl: 535 | "http://localhost:3000/api/auth/oauth2/authorize", 536 | tokenUrl: "http://localhost:3000/api/auth/oauth2/token", 537 | scopes: ["openid", "profile", "email"], 538 | pkce: true, 539 | }, 540 | ], 541 | }), 542 | ], 543 | }); 544 | 545 | const client = createAuthClient({ 546 | plugins: [genericOAuthClient()], 547 | baseURL: "http://localhost:5000", 548 | fetchOptions: { 549 | customFetchImpl: customFetchImplRP, 550 | }, 551 | }); 552 | const oAuthHeaders = new Headers(); 553 | const data = await client.signIn.oauth2( 554 | { 555 | providerId: "test", 556 | callbackURL: "/dashboard", 557 | }, 558 | { 559 | throw: true, 560 | onSuccess: cookieSetter(oAuthHeaders), 561 | }, 562 | ); 563 | expect(data.url).toContain( 564 | "http://localhost:3000/api/auth/oauth2/authorize", 565 | ); 566 | expect(data.url).toContain(`client_id=${application.clientId}`); 567 | 568 | let redirectURI = ""; 569 | const newHeaders = new Headers(); 570 | await serverClient.$fetch(data.url, { 571 | method: "GET", 572 | onError(context) { 573 | redirectURI = context.response.headers.get("Location") || ""; 574 | cookieSetter(newHeaders)(context); 575 | // Note: headers might be available from parent scope (serverClient auth) 576 | // newHeaders already has the consent cookies 577 | }, 578 | }); 579 | 580 | // Handle consent flow if required (per OIDC spec for non-trusted clients) 581 | redirectURI = await handleConsentFlow( 582 | redirectURI, 583 | serverClient, 584 | headers, 585 | newHeaders, 586 | ); 587 | 588 | // Verify we got an authorization code 589 | expect(redirectURI).toContain( 590 | "http://localhost:3000/api/auth/oauth2/callback/test?code=", 591 | ); 592 | 593 | let callbackURL = ""; 594 | await client.$fetch(redirectURI, { 595 | headers: oAuthHeaders, 596 | onError(context) { 597 | callbackURL = context.response.headers.get("Location") || ""; 598 | }, 599 | }); 600 | expect(callbackURL).toContain("/dashboard"); 601 | }); 602 | }); 603 | 604 | describe("oidc-jwt", async () => { 605 | let server: Listener | null = null; 606 | 607 | afterEach(async () => { 608 | if (server) { 609 | await server.close(); 610 | server = null; 611 | } 612 | }); 613 | 614 | test.each([ 615 | { useJwt: true, description: "with jwt plugin", expected: "EdDSA" }, 616 | { useJwt: false, description: "without jwt plugin", expected: "HS256" }, 617 | ])( 618 | "testing oidc-provider $description to return token signed with $expected", 619 | async ({ useJwt, expected }) => { 620 | const { 621 | auth: authorizationServer, 622 | signInWithTestUser, 623 | customFetchImpl, 624 | testUser, 625 | } = await getTestInstance({ 626 | baseURL: "http://localhost:3000", 627 | plugins: [ 628 | oidcProvider({ 629 | loginPage: "/login", 630 | consentPage: "/oauth2/authorize", 631 | requirePKCE: true, 632 | getAdditionalUserInfoClaim(user) { 633 | return { 634 | custom: "custom value", 635 | userId: user.id, 636 | }; 637 | }, 638 | useJWTPlugin: useJwt, 639 | }), 640 | ...(useJwt ? [jwt()] : []), 641 | ], 642 | }); 643 | const { headers } = await signInWithTestUser(); 644 | const serverClient = createAuthClient({ 645 | plugins: [oidcClient()], 646 | baseURL: "http://localhost:3000", 647 | fetchOptions: { 648 | customFetchImpl, 649 | headers, 650 | }, 651 | }); 652 | server = await listen(toNodeHandler(authorizationServer.handler), { 653 | port: 3000, 654 | }); 655 | let application: Client = { 656 | clientId: "test-client-id", 657 | clientSecret: "test-client-secret-oidc", 658 | redirectURLs: ["http://localhost:3000/api/auth/oauth2/callback/test"], 659 | metadata: {}, 660 | icon: "", 661 | type: "web", 662 | disabled: false, 663 | name: "test", 664 | }; 665 | const createdClient = await serverClient.oauth2.register({ 666 | client_name: application.name, 667 | redirect_uris: application.redirectURLs, 668 | logo_uri: application.icon, 669 | }); 670 | expect(createdClient.data).toMatchObject({ 671 | client_id: expect.any(String), 672 | client_secret: expect.any(String), 673 | client_name: "test", 674 | logo_uri: "", 675 | redirect_uris: ["http://localhost:3000/api/auth/oauth2/callback/test"], 676 | grant_types: ["authorization_code"], 677 | response_types: ["code"], 678 | token_endpoint_auth_method: "client_secret_basic", 679 | client_id_issued_at: expect.any(Number), 680 | client_secret_expires_at: 0, 681 | }); 682 | if (createdClient.data) { 683 | application = { 684 | clientId: createdClient.data.client_id, 685 | clientSecret: createdClient.data.client_secret, 686 | redirectURLs: createdClient.data.redirect_uris, 687 | metadata: {}, 688 | icon: createdClient.data.logo_uri || "", 689 | type: "web", 690 | disabled: false, 691 | name: createdClient.data.client_name || "", 692 | }; 693 | } 694 | 695 | // The RP (Relying Party) - the client application 696 | const { customFetchImpl: customFetchImplRP, cookieSetter } = 697 | await getTestInstance({ 698 | account: { 699 | accountLinking: { 700 | trustedProviders: ["test"], 701 | }, 702 | }, 703 | plugins: [ 704 | genericOAuth({ 705 | config: [ 706 | { 707 | providerId: "test", 708 | clientId: application.clientId, 709 | clientSecret: application.clientSecret || "", 710 | authorizationUrl: 711 | "http://localhost:3000/api/auth/oauth2/authorize", 712 | tokenUrl: "http://localhost:3000/api/auth/oauth2/token", 713 | scopes: ["openid", "profile", "email"], 714 | pkce: true, 715 | }, 716 | ], 717 | }), 718 | ], 719 | }); 720 | 721 | const client = createAuthClient({ 722 | plugins: [genericOAuthClient()], 723 | baseURL: "http://localhost:5000", 724 | fetchOptions: { 725 | customFetchImpl: customFetchImplRP, 726 | }, 727 | }); 728 | const oAuthHeaders = new Headers(); 729 | const data = await client.signIn.oauth2( 730 | { 731 | providerId: "test", 732 | callbackURL: "/dashboard", 733 | }, 734 | { 735 | throw: true, 736 | onSuccess: cookieSetter(oAuthHeaders), 737 | }, 738 | ); 739 | expect(data.url).toContain( 740 | "http://localhost:3000/api/auth/oauth2/authorize", 741 | ); 742 | expect(data.url).toContain(`client_id=${application.clientId}`); 743 | 744 | let redirectURI = ""; 745 | const newHeaders = new Headers(); 746 | await serverClient.$fetch(data.url, { 747 | method: "GET", 748 | onError(context) { 749 | redirectURI = context.response.headers.get("Location") || ""; 750 | cookieSetter(newHeaders)(context); 751 | if (headers.get("Cookie")) { 752 | newHeaders.append("Cookie", headers.get("Cookie") || ""); 753 | } 754 | }, 755 | }); 756 | 757 | // Check if consent is needed (per OIDC spec) 758 | if (redirectURI.includes("consent_code=")) { 759 | // Handle consent flow - this is expected per OIDC spec for non-trusted clients 760 | expect(redirectURI).toContain("/oauth2/authorize?"); 761 | expect(redirectURI).toContain("consent_code="); 762 | expect(redirectURI).toContain("client_id="); 763 | 764 | // Extract consent_code from URL 765 | const url = new URL(redirectURI, "http://localhost:3000"); 766 | const consentCode = url.searchParams.get("consent_code"); 767 | 768 | const res = await serverClient.oauth2.consent( 769 | { 770 | accept: true, 771 | consent_code: consentCode, 772 | }, 773 | { 774 | headers: newHeaders, 775 | throw: true, 776 | }, 777 | ); 778 | expect(res.redirectURI).toContain( 779 | "http://localhost:3000/api/auth/oauth2/callback/test?code=", 780 | ); 781 | redirectURI = res.redirectURI; 782 | } else { 783 | // Direct code response (trusted client) 784 | expect(redirectURI).toContain( 785 | "http://localhost:3000/api/auth/oauth2/callback/test?code=", 786 | ); 787 | } 788 | let authToken = undefined; 789 | let callbackURL = ""; 790 | await client.$fetch(redirectURI, { 791 | headers: oAuthHeaders, 792 | onError(context) { 793 | callbackURL = context.response.headers.get("Location") || ""; 794 | authToken = context.response.headers.get("set-auth-token")!; 795 | }, 796 | }); 797 | expect(callbackURL).toContain("/dashboard"); 798 | const accessToken = await client.getAccessToken( 799 | { providerId: "test", userId: testUser.id }, 800 | { 801 | auth: { 802 | type: "Bearer", 803 | token: authToken, 804 | }, 805 | }, 806 | ); 807 | const decoded = decodeProtectedHeader(accessToken.data?.idToken!); 808 | if (useJwt) { 809 | const jwks = await authorizationServer.api.getJwks(); 810 | const jwkSet = createLocalJWKSet(jwks); 811 | const checkSignature = await jwtVerify( 812 | accessToken.data?.idToken!, 813 | jwkSet, 814 | ); 815 | expect(checkSignature).toBeDefined(); 816 | expect(Number.isInteger(checkSignature.payload.iat)).toBeTruthy(); 817 | expect(Number.isInteger(checkSignature.payload.exp)).toBeTruthy(); 818 | } else { 819 | const clientSecret = application.clientSecret; 820 | const checkSignature = await jwtVerify( 821 | accessToken.data?.idToken!, 822 | new TextEncoder().encode(clientSecret), 823 | ); 824 | expect(checkSignature).toBeDefined(); 825 | } 826 | 827 | // expect(checkSignature.payload).toBeDefined(); 828 | expect(decoded.alg).toBe(expected); 829 | }, 830 | ); 831 | }); 832 | ``` -------------------------------------------------------------------------------- /packages/better-auth/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "better-auth", 3 | "version": "1.4.0-beta.10", 4 | "description": "The most comprehensive authentication library for TypeScript.", 5 | "type": "module", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/better-auth/better-auth", 10 | "directory": "packages/better-auth" 11 | }, 12 | "keywords": [ 13 | "auth", 14 | "oauth", 15 | "oidc", 16 | "2fa", 17 | "social", 18 | "security", 19 | "typescript", 20 | "nextjs" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "tsdown", 27 | "test": "vitest", 28 | "prepare": "prisma generate --schema ./src/adapters/prisma-adapter/test/base.prisma", 29 | "typecheck": "tsc --project tsconfig.json" 30 | }, 31 | "main": "./dist/index.js", 32 | "module": "./dist/index.js", 33 | "exports": { 34 | ".": { 35 | "import": { 36 | "types": "./dist/index.d.ts", 37 | "default": "./dist/index.js" 38 | }, 39 | "require": { 40 | "types": "./dist/index.d.cts", 41 | "default": "./dist/index.cjs" 42 | } 43 | }, 44 | "./social-providers": { 45 | "import": { 46 | "types": "./dist/social-providers/index.d.ts", 47 | "default": "./dist/social-providers/index.js" 48 | }, 49 | "require": { 50 | "types": "./dist/social-providers/index.d.cts", 51 | "default": "./dist/social-providers/index.cjs" 52 | } 53 | }, 54 | "./client": { 55 | "import": { 56 | "types": "./dist/client/index.d.ts", 57 | "default": "./dist/client/index.js" 58 | }, 59 | "require": { 60 | "types": "./dist/client/index.d.cts", 61 | "default": "./dist/client/index.cjs" 62 | } 63 | }, 64 | "./client/plugins": { 65 | "import": { 66 | "types": "./dist/client/plugins/index.d.ts", 67 | "default": "./dist/client/plugins/index.js" 68 | }, 69 | "require": { 70 | "types": "./dist/client/plugins/index.d.cts", 71 | "default": "./dist/client/plugins/index.cjs" 72 | } 73 | }, 74 | "./types": { 75 | "import": { 76 | "types": "./dist/types/index.d.ts", 77 | "default": "./dist/types/index.js" 78 | }, 79 | "require": { 80 | "types": "./dist/types/index.d.cts", 81 | "default": "./dist/types/index.cjs" 82 | } 83 | }, 84 | "./crypto": { 85 | "import": { 86 | "types": "./dist/crypto/index.d.ts", 87 | "default": "./dist/crypto/index.js" 88 | }, 89 | "require": { 90 | "types": "./dist/crypto/index.d.cts", 91 | "default": "./dist/crypto/index.cjs" 92 | } 93 | }, 94 | "./cookies": { 95 | "import": { 96 | "types": "./dist/cookies/index.d.ts", 97 | "default": "./dist/cookies/index.js" 98 | }, 99 | "require": { 100 | "types": "./dist/cookies/index.d.cts", 101 | "default": "./dist/cookies/index.cjs" 102 | } 103 | }, 104 | "./oauth2": { 105 | "import": { 106 | "types": "./dist/oauth2/index.d.ts", 107 | "default": "./dist/oauth2/index.js" 108 | }, 109 | "require": { 110 | "types": "./dist/oauth2/index.d.cts", 111 | "default": "./dist/oauth2/index.cjs" 112 | } 113 | }, 114 | "./react": { 115 | "import": { 116 | "types": "./dist/client/react/index.d.ts", 117 | "default": "./dist/client/react/index.js" 118 | }, 119 | "require": { 120 | "types": "./dist/client/react/index.d.cts", 121 | "default": "./dist/client/react/index.cjs" 122 | } 123 | }, 124 | "./solid": { 125 | "import": { 126 | "types": "./dist/client/solid/index.d.ts", 127 | "default": "./dist/client/solid/index.js" 128 | }, 129 | "require": { 130 | "types": "./dist/client/solid/index.d.cts", 131 | "default": "./dist/client/solid/index.cjs" 132 | } 133 | }, 134 | "./lynx": { 135 | "import": { 136 | "types": "./dist/client/lynx/index.d.ts", 137 | "default": "./dist/client/lynx/index.js" 138 | }, 139 | "require": { 140 | "types": "./dist/client/lynx/index.d.cts", 141 | "default": "./dist/client/lynx/index.cjs" 142 | } 143 | }, 144 | "./test": { 145 | "import": { 146 | "types": "./dist/test-utils/index.d.ts", 147 | "default": "./dist/test-utils/index.js" 148 | }, 149 | "require": { 150 | "types": "./dist/test-utils/index.d.cts", 151 | "default": "./dist/test-utils/index.cjs" 152 | } 153 | }, 154 | "./api": { 155 | "import": { 156 | "types": "./dist/api/index.d.ts", 157 | "default": "./dist/api/index.js" 158 | }, 159 | "require": { 160 | "types": "./dist/api/index.d.cts", 161 | "default": "./dist/api/index.cjs" 162 | } 163 | }, 164 | "./db": { 165 | "import": { 166 | "types": "./dist/db/index.d.ts", 167 | "default": "./dist/db/index.js" 168 | }, 169 | "require": { 170 | "types": "./dist/db/index.d.cts", 171 | "default": "./dist/db/index.cjs" 172 | } 173 | }, 174 | "./vue": { 175 | "import": { 176 | "types": "./dist/client/vue/index.d.ts", 177 | "default": "./dist/client/vue/index.js" 178 | }, 179 | "require": { 180 | "types": "./dist/client/vue/index.d.cts", 181 | "default": "./dist/client/vue/index.cjs" 182 | } 183 | }, 184 | "./plugins": { 185 | "import": { 186 | "types": "./dist/plugins/index.d.ts", 187 | "default": "./dist/plugins/index.js" 188 | }, 189 | "require": { 190 | "types": "./dist/plugins/index.d.cts", 191 | "default": "./dist/plugins/index.cjs" 192 | } 193 | }, 194 | "./svelte-kit": { 195 | "import": { 196 | "types": "./dist/integrations/svelte-kit.d.ts", 197 | "default": "./dist/integrations/svelte-kit.js" 198 | }, 199 | "require": { 200 | "types": "./dist/integrations/svelte-kit.d.cts", 201 | "default": "./dist/integrations/svelte-kit.cjs" 202 | } 203 | }, 204 | "./solid-start": { 205 | "import": { 206 | "types": "./dist/integrations/solid-start.d.ts", 207 | "default": "./dist/integrations/solid-start.js" 208 | }, 209 | "require": { 210 | "types": "./dist/integrations/solid-start.d.cts", 211 | "default": "./dist/integrations/solid-start.cjs" 212 | } 213 | }, 214 | "./svelte": { 215 | "import": { 216 | "types": "./dist/client/svelte/index.d.ts", 217 | "default": "./dist/client/svelte/index.js" 218 | }, 219 | "require": { 220 | "types": "./dist/client/svelte/index.d.cts", 221 | "default": "./dist/client/svelte/index.cjs" 222 | } 223 | }, 224 | "./next-js": { 225 | "import": { 226 | "types": "./dist/integrations/next-js.d.ts", 227 | "default": "./dist/integrations/next-js.js" 228 | }, 229 | "require": { 230 | "types": "./dist/integrations/next-js.d.cts", 231 | "default": "./dist/integrations/next-js.cjs" 232 | } 233 | }, 234 | "./react-start": { 235 | "import": { 236 | "types": "./dist/integrations/react-start.d.ts", 237 | "default": "./dist/integrations/react-start.js" 238 | }, 239 | "require": { 240 | "types": "./dist/integrations/react-start.d.cts", 241 | "default": "./dist/integrations/react-start.cjs" 242 | } 243 | }, 244 | "./node": { 245 | "import": { 246 | "types": "./dist/integrations/node.d.ts", 247 | "default": "./dist/integrations/node.js" 248 | }, 249 | "require": { 250 | "types": "./dist/integrations/node.d.cts", 251 | "default": "./dist/integrations/node.cjs" 252 | } 253 | }, 254 | "./adapters/prisma": { 255 | "import": { 256 | "types": "./dist/adapters/prisma-adapter/index.d.ts", 257 | "default": "./dist/adapters/prisma-adapter/index.js" 258 | }, 259 | "require": { 260 | "types": "./dist/adapters/prisma-adapter/index.d.cts", 261 | "default": "./dist/adapters/prisma-adapter/index.cjs" 262 | } 263 | }, 264 | "./adapters/drizzle": { 265 | "import": { 266 | "types": "./dist/adapters/drizzle-adapter/index.d.ts", 267 | "default": "./dist/adapters/drizzle-adapter/index.js" 268 | }, 269 | "require": { 270 | "types": "./dist/adapters/drizzle-adapter/index.d.cts", 271 | "default": "./dist/adapters/drizzle-adapter/index.cjs" 272 | } 273 | }, 274 | "./adapters/mongodb": { 275 | "import": { 276 | "types": "./dist/adapters/mongodb-adapter/index.d.ts", 277 | "default": "./dist/adapters/mongodb-adapter/index.js" 278 | }, 279 | "require": { 280 | "types": "./dist/adapters/mongodb-adapter/index.d.cts", 281 | "default": "./dist/adapters/mongodb-adapter/index.cjs" 282 | } 283 | }, 284 | "./adapters/memory": { 285 | "import": { 286 | "types": "./dist/adapters/memory-adapter/index.d.ts", 287 | "default": "./dist/adapters/memory-adapter/index.js" 288 | }, 289 | "require": { 290 | "types": "./dist/adapters/memory-adapter/index.d.cts", 291 | "default": "./dist/adapters/memory-adapter/index.cjs" 292 | } 293 | }, 294 | "./adapters/test": { 295 | "import": { 296 | "types": "./dist/adapters/test.d.ts", 297 | "default": "./dist/adapters/test.js" 298 | }, 299 | "require": { 300 | "types": "./dist/adapters/test.d.cts", 301 | "default": "./dist/adapters/test.cjs" 302 | } 303 | }, 304 | "./adapters": { 305 | "import": { 306 | "types": "./dist/adapters/index.d.ts", 307 | "default": "./dist/adapters/index.js" 308 | }, 309 | "require": { 310 | "types": "./dist/adapters/index.d.cts", 311 | "default": "./dist/adapters/index.cjs" 312 | } 313 | }, 314 | "./plugins/access": { 315 | "import": { 316 | "types": "./dist/plugins/access/index.d.ts", 317 | "default": "./dist/plugins/access/index.js" 318 | }, 319 | "require": { 320 | "types": "./dist/plugins/access/index.d.cts", 321 | "default": "./dist/plugins/access/index.cjs" 322 | } 323 | }, 324 | "./plugins/admin": { 325 | "import": { 326 | "types": "./dist/plugins/admin/index.d.ts", 327 | "default": "./dist/plugins/admin/index.js" 328 | }, 329 | "require": { 330 | "types": "./dist/plugins/admin/index.d.cts", 331 | "default": "./dist/plugins/admin/index.cjs" 332 | } 333 | }, 334 | "./plugins/admin/access": { 335 | "import": { 336 | "types": "./dist/plugins/admin/access/index.d.ts", 337 | "default": "./dist/plugins/admin/access/index.js" 338 | }, 339 | "require": { 340 | "types": "./dist/plugins/admin/access/index.d.cts", 341 | "default": "./dist/plugins/admin/access/index.cjs" 342 | } 343 | }, 344 | "./plugins/anonymous": { 345 | "import": { 346 | "types": "./dist/plugins/anonymous/index.d.ts", 347 | "default": "./dist/plugins/anonymous/index.js" 348 | }, 349 | "require": { 350 | "types": "./dist/plugins/anonymous/index.d.cts", 351 | "default": "./dist/plugins/anonymous/index.cjs" 352 | } 353 | }, 354 | "./plugins/bearer": { 355 | "import": { 356 | "types": "./dist/plugins/bearer/index.d.ts", 357 | "default": "./dist/plugins/bearer/index.js" 358 | }, 359 | "require": { 360 | "types": "./dist/plugins/bearer/index.d.cts", 361 | "default": "./dist/plugins/bearer/index.cjs" 362 | } 363 | }, 364 | "./plugins/custom-session": { 365 | "import": { 366 | "types": "./dist/plugins/custom-session/index.d.ts", 367 | "default": "./dist/plugins/custom-session/index.js" 368 | }, 369 | "require": { 370 | "types": "./dist/plugins/custom-session/index.d.cts", 371 | "default": "./dist/plugins/custom-session/index.cjs" 372 | } 373 | }, 374 | "./plugins/email-otp": { 375 | "import": { 376 | "types": "./dist/plugins/email-otp/index.d.ts", 377 | "default": "./dist/plugins/email-otp/index.js" 378 | }, 379 | "require": { 380 | "types": "./dist/plugins/email-otp/index.d.cts", 381 | "default": "./dist/plugins/email-otp/index.cjs" 382 | } 383 | }, 384 | "./plugins/generic-oauth": { 385 | "import": { 386 | "types": "./dist/plugins/generic-oauth/index.d.ts", 387 | "default": "./dist/plugins/generic-oauth/index.js" 388 | }, 389 | "require": { 390 | "types": "./dist/plugins/generic-oauth/index.d.cts", 391 | "default": "./dist/plugins/generic-oauth/index.cjs" 392 | } 393 | }, 394 | "./plugins/jwt": { 395 | "import": { 396 | "types": "./dist/plugins/jwt/index.d.ts", 397 | "default": "./dist/plugins/jwt/index.js" 398 | }, 399 | "require": { 400 | "types": "./dist/plugins/jwt/index.d.cts", 401 | "default": "./dist/plugins/jwt/index.cjs" 402 | } 403 | }, 404 | "./plugins/haveibeenpwned": { 405 | "import": { 406 | "types": "./dist/plugins/haveibeenpwned/index.d.ts", 407 | "default": "./dist/plugins/haveibeenpwned/index.js" 408 | }, 409 | "require": { 410 | "types": "./dist/plugins/haveibeenpwned/index.d.cts", 411 | "default": "./dist/plugins/haveibeenpwned/index.cjs" 412 | } 413 | }, 414 | "./plugins/sso": { 415 | "import": { 416 | "types": "./dist/plugins/sso/index.d.ts", 417 | "default": "./dist/plugins/sso/index.js" 418 | }, 419 | "require": { 420 | "types": "./dist/plugins/sso/index.d.cts", 421 | "default": "./dist/plugins/sso/index.cjs" 422 | } 423 | }, 424 | "./plugins/oidc-provider": { 425 | "import": { 426 | "types": "./dist/plugins/oidc-provider/index.d.ts", 427 | "default": "./dist/plugins/oidc-provider/index.js" 428 | }, 429 | "require": { 430 | "types": "./dist/plugins/oidc-provider/index.d.cts", 431 | "default": "./dist/plugins/oidc-provider/index.cjs" 432 | } 433 | }, 434 | "./plugins/magic-link": { 435 | "import": { 436 | "types": "./dist/plugins/magic-link/index.d.ts", 437 | "default": "./dist/plugins/magic-link/index.js" 438 | }, 439 | "require": { 440 | "types": "./dist/plugins/magic-link/index.d.cts", 441 | "default": "./dist/plugins/magic-link/index.cjs" 442 | } 443 | }, 444 | "./plugins/multi-session": { 445 | "import": { 446 | "types": "./dist/plugins/multi-session/index.d.ts", 447 | "default": "./dist/plugins/multi-session/index.js" 448 | }, 449 | "require": { 450 | "types": "./dist/plugins/multi-session/index.d.cts", 451 | "default": "./dist/plugins/multi-session/index.cjs" 452 | } 453 | }, 454 | "./plugins/oauth-proxy": { 455 | "import": { 456 | "types": "./dist/plugins/oauth-proxy/index.d.ts", 457 | "default": "./dist/plugins/oauth-proxy/index.js" 458 | }, 459 | "require": { 460 | "types": "./dist/plugins/oauth-proxy/index.d.cts", 461 | "default": "./dist/plugins/oauth-proxy/index.cjs" 462 | } 463 | }, 464 | "./plugins/organization": { 465 | "import": { 466 | "types": "./dist/plugins/organization/index.d.ts", 467 | "default": "./dist/plugins/organization/index.js" 468 | }, 469 | "require": { 470 | "types": "./dist/plugins/organization/index.d.cts", 471 | "default": "./dist/plugins/organization/index.cjs" 472 | } 473 | }, 474 | "./plugins/organization/access": { 475 | "import": { 476 | "types": "./dist/plugins/organization/access/index.d.ts", 477 | "default": "./dist/plugins/organization/access/index.js" 478 | }, 479 | "require": { 480 | "types": "./dist/plugins/organization/access/index.d.cts", 481 | "default": "./dist/plugins/organization/access/index.cjs" 482 | } 483 | }, 484 | "./plugins/one-time-token": { 485 | "import": { 486 | "types": "./dist/plugins/one-time-token/index.d.ts", 487 | "default": "./dist/plugins/one-time-token/index.js" 488 | }, 489 | "require": { 490 | "types": "./dist/plugins/one-time-token/index.d.cts", 491 | "default": "./dist/plugins/one-time-token/index.cjs" 492 | } 493 | }, 494 | "./plugins/passkey": { 495 | "import": { 496 | "types": "./dist/plugins/passkey/index.d.ts", 497 | "default": "./dist/plugins/passkey/index.js" 498 | }, 499 | "require": { 500 | "types": "./dist/plugins/passkey/index.d.cts", 501 | "default": "./dist/plugins/passkey/index.cjs" 502 | } 503 | }, 504 | "./plugins/phone-number": { 505 | "import": { 506 | "types": "./dist/plugins/phone-number/index.d.ts", 507 | "default": "./dist/plugins/phone-number/index.js" 508 | }, 509 | "require": { 510 | "types": "./dist/plugins/phone-number/index.d.cts", 511 | "default": "./dist/plugins/phone-number/index.cjs" 512 | } 513 | }, 514 | "./plugins/two-factor": { 515 | "import": { 516 | "types": "./dist/plugins/two-factor/index.d.ts", 517 | "default": "./dist/plugins/two-factor/index.js" 518 | }, 519 | "require": { 520 | "types": "./dist/plugins/two-factor/index.d.cts", 521 | "default": "./dist/plugins/two-factor/index.cjs" 522 | } 523 | }, 524 | "./plugins/username": { 525 | "import": { 526 | "types": "./dist/plugins/username/index.d.ts", 527 | "default": "./dist/plugins/username/index.js" 528 | }, 529 | "require": { 530 | "types": "./dist/plugins/username/index.d.cts", 531 | "default": "./dist/plugins/username/index.cjs" 532 | } 533 | }, 534 | "./plugins/siwe": { 535 | "import": { 536 | "types": "./dist/plugins/siwe/index.d.ts", 537 | "default": "./dist/plugins/siwe/index.js" 538 | }, 539 | "require": { 540 | "types": "./dist/plugins/siwe/index.d.cts", 541 | "default": "./dist/plugins/siwe/index.cjs" 542 | } 543 | }, 544 | "./plugins/device-authorization": { 545 | "import": { 546 | "types": "./dist/plugins/device-authorization/index.d.ts", 547 | "default": "./dist/plugins/device-authorization/index.js" 548 | }, 549 | "require": { 550 | "types": "./dist/plugins/device-authorization/index.d.cts", 551 | "default": "./dist/plugins/device-authorization/index.cjs" 552 | } 553 | } 554 | }, 555 | "typesVersions": { 556 | "*": { 557 | "*": [ 558 | "./dist/index.d.ts" 559 | ], 560 | "node": [ 561 | "./dist/integrations/node.d.ts" 562 | ], 563 | "react": [ 564 | "./dist/client/react/index.d.ts" 565 | ], 566 | "vue": [ 567 | "./dist/client/vue/index.d.ts" 568 | ], 569 | "svelte": [ 570 | "./dist/client/svelte/index.d.ts" 571 | ], 572 | "social-providers": [ 573 | "./dist/social-providers/index.d.ts" 574 | ], 575 | "client": [ 576 | "./dist/client/index.d.ts" 577 | ], 578 | "client/plugins": [ 579 | "./dist/client/plugins/index.d.ts" 580 | ], 581 | "types": [ 582 | "./dist/types/index.d.ts" 583 | ], 584 | "crypto": [ 585 | "./dist/crypto/index.d.ts" 586 | ], 587 | "cookies": [ 588 | "./dist/cookies/index.d.ts" 589 | ], 590 | "oauth2": [ 591 | "./dist/oauth2/index.d.ts" 592 | ], 593 | "solid": [ 594 | "./dist/client/solid/index.d.ts" 595 | ], 596 | "lynx": [ 597 | "./dist/client/lynx/index.d.ts" 598 | ], 599 | "api": [ 600 | "./dist/api/index.d.ts" 601 | ], 602 | "db": [ 603 | "./dist/db/index.d.ts" 604 | ], 605 | "svelte-kit": [ 606 | "./dist/integrations/svelte-kit.d.ts" 607 | ], 608 | "solid-start": [ 609 | "./dist/integrations/solid-start.d.ts" 610 | ], 611 | "next-js": [ 612 | "./dist/integrations/next-js.d.ts" 613 | ], 614 | "react-start": [ 615 | "./dist/integrations/react-start.d.ts" 616 | ], 617 | "adapters": [ 618 | "./dist/adapters/index.d.ts" 619 | ], 620 | "adapters/prisma": [ 621 | "./dist/adapters/prisma-adapter/index.d.ts" 622 | ], 623 | "adapters/drizzle": [ 624 | "./dist/adapters/drizzle-adapter/index.d.ts" 625 | ], 626 | "adapters/mongodb": [ 627 | "./dist/adapters/mongodb-adapter/index.d.ts" 628 | ], 629 | "adapters/memory": [ 630 | "./dist/adapters/memory-adapter/index.d.ts" 631 | ], 632 | "plugins": [ 633 | "./dist/plugins/index.d.ts" 634 | ], 635 | "plugins/access": [ 636 | "./dist/plugins/access/index.d.ts" 637 | ], 638 | "plugins/admin": [ 639 | "./dist/plugins/admin/index.d.ts" 640 | ], 641 | "plugins/admin/access": [ 642 | "./dist/plugins/admin/access/index.d.ts" 643 | ], 644 | "plugins/anonymous": [ 645 | "./dist/plugins/anonymous/index.d.ts" 646 | ], 647 | "plugins/bearer": [ 648 | "./dist/plugins/bearer/index.d.ts" 649 | ], 650 | "plugins/custom-session": [ 651 | "./dist/plugins/custom-session/index.d.ts" 652 | ], 653 | "plugins/email-otp": [ 654 | "./dist/plugins/email-otp/index.d.ts" 655 | ], 656 | "plugins/generic-oauth": [ 657 | "./dist/plugins/generic-oauth/index.d.ts" 658 | ], 659 | "plugins/haveibeenpwned": [ 660 | "./dist/plugins/haveibeenpwned/index.d.ts" 661 | ], 662 | "plugins/oauth-proxy": [ 663 | "./dist/plugins/oauth-proxy/index.d.ts" 664 | ], 665 | "plugins/one-time-token": [ 666 | "./dist/plugins/one-time-token/index.d.ts" 667 | ], 668 | "plugins/sso": [ 669 | "./dist/plugins/sso/index.d.ts" 670 | ], 671 | "plugins/oidc-provider": [ 672 | "./dist/plugins/oidc-provider/index.d.ts" 673 | ], 674 | "plugins/jwt": [ 675 | "./dist/plugins/jwt/index.d.ts" 676 | ], 677 | "plugins/magic-link": [ 678 | "./dist/plugins/magic-link/index.d.ts" 679 | ], 680 | "plugins/organization": [ 681 | "./dist/plugins/organization/index.d.ts" 682 | ], 683 | "plugins/organization/access": [ 684 | "./dist/plugins/organization/access/index.d.ts" 685 | ], 686 | "plugins/passkey": [ 687 | "./dist/plugins/passkey/index.d.ts" 688 | ], 689 | "plugins/phone-number": [ 690 | "./dist/plugins/phone-number/index.d.ts" 691 | ], 692 | "plugins/two-factor": [ 693 | "./dist/plugins/two-factor/index.d.ts" 694 | ], 695 | "plugins/username": [ 696 | "./dist/plugins/username/index.d.ts" 697 | ], 698 | "plugins/siwe": [ 699 | "./dist/plugins/siwe/index.d.ts" 700 | ], 701 | "plugins/device-authorization": [ 702 | "./dist/plugins/device-authorization/index.d.ts" 703 | ] 704 | } 705 | }, 706 | "dependencies": { 707 | "@better-auth/core": "workspace:*", 708 | "@better-auth/telemetry": "workspace:*", 709 | "@better-auth/utils": "0.3.0", 710 | "@better-fetch/fetch": "catalog:", 711 | "@noble/ciphers": "^2.0.0", 712 | "@noble/hashes": "^2.0.0", 713 | "@simplewebauthn/browser": "^13.1.2", 714 | "@simplewebauthn/server": "^13.1.2", 715 | "better-call": "catalog:", 716 | "defu": "^6.1.4", 717 | "jose": "^6.1.0", 718 | "kysely": "^0.28.5", 719 | "nanostores": "^1.0.1", 720 | "zod": "^4.1.5" 721 | }, 722 | "peerDependenciesOptional": { 723 | "@lynx-js/react": "*", 724 | "@sveltejs/kit": "^2.0.0", 725 | "next": "^14.0.0 || ^15.0.0", 726 | "react": "^18.0.0 || ^19.0.0", 727 | "react-dom": "^18.0.0 || ^19.0.0", 728 | "solid-js": "^1.0.0", 729 | "svelte": "^4.0.0 || ^5.0.0", 730 | "vue": "^3.0.0" 731 | }, 732 | "peerDependenciesMeta": { 733 | "@lynx-js/react": { 734 | "optional": true 735 | }, 736 | "@sveltejs/kit": { 737 | "optional": true 738 | }, 739 | "next": { 740 | "optional": true 741 | }, 742 | "react": { 743 | "optional": true 744 | }, 745 | "react-dom": { 746 | "optional": true 747 | }, 748 | "solid-js": { 749 | "optional": true 750 | }, 751 | "svelte": { 752 | "optional": true 753 | }, 754 | "vue": { 755 | "optional": true 756 | } 757 | }, 758 | "devDependencies": { 759 | "@lynx-js/react": "^0.114.0", 760 | "@prisma/client": "^5.22.0", 761 | "@sveltejs/kit": "^2.37.1", 762 | "@tanstack/react-start": "^1.131.3", 763 | "@tanstack/start-server-core": "^1.131.36", 764 | "@types/better-sqlite3": "^7.6.13", 765 | "@types/bun": "^1.2.23", 766 | "@types/keccak": "^3.0.5", 767 | "@types/pg": "^8.15.5", 768 | "@types/prompts": "^2.4.9", 769 | "@types/react": "^19.2.2", 770 | "better-sqlite3": "^12.2.0", 771 | "concurrently": "^9.2.1", 772 | "deepmerge": "^4.3.1", 773 | "drizzle-kit": "^0.31.4", 774 | "drizzle-orm": "^0.38.2", 775 | "happy-dom": "^20.0.0", 776 | "hono": "^4.9.7", 777 | "listhen": "^1.9.0", 778 | "mongodb": "^6.18.0", 779 | "ms": "4.0.0-nightly.202508271359", 780 | "msw": "^2.11.5", 781 | "mysql2": "^3.14.4", 782 | "next": "^15.5.4", 783 | "oauth2-mock-server": "^7.2.1", 784 | "pg": "^8.16.3", 785 | "prisma": "^5.22.0", 786 | "react": "^19.2.0", 787 | "react-dom": "^19.2.0", 788 | "react-native": "~0.80.2", 789 | "solid-js": "^1.9.8", 790 | "tarn": "^3.0.2", 791 | "tedious": "^18.6.1", 792 | "tsdown": "catalog:", 793 | "type-fest": "^4.41.0", 794 | "typescript": "catalog:", 795 | "vue": "^3.5.18" 796 | }, 797 | "files": [ 798 | "dist" 799 | ] 800 | } 801 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/routes/crud-org.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { createAuthEndpoint } from "@better-auth/core/middleware"; 3 | import { getOrgAdapter } from "../adapter"; 4 | import { orgMiddleware, orgSessionMiddleware } from "../call"; 5 | import { APIError } from "better-call"; 6 | import { setSessionCookie } from "../../../cookies"; 7 | import { ORGANIZATION_ERROR_CODES } from "../error-codes"; 8 | import { getSessionFromCtx, requestOnlySessionMiddleware } from "../../../api"; 9 | import type { OrganizationOptions } from "../types"; 10 | import type { 11 | InferInvitation, 12 | InferMember, 13 | InferOrganization, 14 | Member, 15 | Team, 16 | TeamMember, 17 | } from "../schema"; 18 | import { hasPermission } from "../has-permission"; 19 | import { 20 | toZodSchema, 21 | type InferAdditionalFieldsFromPluginOptions, 22 | } from "../../../db"; 23 | 24 | export const createOrganization = <O extends OrganizationOptions>( 25 | options?: O, 26 | ) => { 27 | const additionalFieldsSchema = toZodSchema({ 28 | fields: options?.schema?.organization?.additionalFields || {}, 29 | isClientSide: true, 30 | }); 31 | const baseSchema = z.object({ 32 | name: z.string().min(1).meta({ 33 | description: "The name of the organization", 34 | }), 35 | slug: z.string().min(1).meta({ 36 | description: "The slug of the organization", 37 | }), 38 | userId: z.coerce 39 | .string() 40 | .meta({ 41 | description: 42 | 'The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server. server-only. Eg: "user-id"', 43 | }) 44 | .optional(), 45 | logo: z 46 | .string() 47 | .meta({ 48 | description: "The logo of the organization", 49 | }) 50 | .optional(), 51 | metadata: z 52 | .record(z.string(), z.any()) 53 | .meta({ 54 | description: "The metadata of the organization", 55 | }) 56 | .optional(), 57 | keepCurrentActiveOrganization: z 58 | .boolean() 59 | .meta({ 60 | description: 61 | "Whether to keep the current active organization active after creating a new one. Eg: true", 62 | }) 63 | .optional(), 64 | }); 65 | 66 | type Body = InferAdditionalFieldsFromPluginOptions<"organization", O> & 67 | z.infer<typeof baseSchema>; 68 | 69 | return createAuthEndpoint( 70 | "/organization/create", 71 | { 72 | method: "POST", 73 | body: z.object({ 74 | ...baseSchema.shape, 75 | ...additionalFieldsSchema.shape, 76 | }), 77 | use: [orgMiddleware], 78 | metadata: { 79 | $Infer: { 80 | body: {} as Body, 81 | }, 82 | openapi: { 83 | description: "Create an organization", 84 | responses: { 85 | "200": { 86 | description: "Success", 87 | content: { 88 | "application/json": { 89 | schema: { 90 | type: "object", 91 | description: "The organization that was created", 92 | $ref: "#/components/schemas/Organization", 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | }, 100 | }, 101 | async (ctx) => { 102 | const session = await getSessionFromCtx(ctx); 103 | 104 | if (!session && (ctx.request || ctx.headers)) { 105 | throw new APIError("UNAUTHORIZED"); 106 | } 107 | let user = session?.user || null; 108 | if (!user) { 109 | if (!ctx.body.userId) { 110 | throw new APIError("UNAUTHORIZED"); 111 | } 112 | user = await ctx.context.internalAdapter.findUserById(ctx.body.userId); 113 | } 114 | if (!user) { 115 | return ctx.json(null, { 116 | status: 401, 117 | }); 118 | } 119 | const options = ctx.context.orgOptions; 120 | const canCreateOrg = 121 | typeof options?.allowUserToCreateOrganization === "function" 122 | ? await options.allowUserToCreateOrganization(user) 123 | : options?.allowUserToCreateOrganization === undefined 124 | ? true 125 | : options.allowUserToCreateOrganization; 126 | 127 | if (!canCreateOrg) { 128 | throw new APIError("FORBIDDEN", { 129 | message: 130 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION, 131 | }); 132 | } 133 | const adapter = getOrgAdapter<O>(ctx.context, options as O); 134 | 135 | const userOrganizations = await adapter.listOrganizations(user.id); 136 | const hasReachedOrgLimit = 137 | typeof options.organizationLimit === "number" 138 | ? userOrganizations.length >= options.organizationLimit 139 | : typeof options.organizationLimit === "function" 140 | ? await options.organizationLimit(user) 141 | : false; 142 | 143 | if (hasReachedOrgLimit) { 144 | throw new APIError("FORBIDDEN", { 145 | message: 146 | ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS, 147 | }); 148 | } 149 | 150 | const existingOrganization = await adapter.findOrganizationBySlug( 151 | ctx.body.slug, 152 | ); 153 | if (existingOrganization) { 154 | throw new APIError("BAD_REQUEST", { 155 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_ALREADY_EXISTS, 156 | }); 157 | } 158 | 159 | let { 160 | keepCurrentActiveOrganization: _, 161 | userId: __, 162 | ...orgData 163 | } = ctx.body; 164 | 165 | if (options.organizationCreation?.beforeCreate) { 166 | const response = await options.organizationCreation.beforeCreate( 167 | { 168 | organization: { 169 | ...orgData, 170 | createdAt: new Date(), 171 | }, 172 | user, 173 | }, 174 | ctx.request, 175 | ); 176 | if (response && typeof response === "object" && "data" in response) { 177 | orgData = { 178 | ...ctx.body, 179 | ...response.data, 180 | }; 181 | } 182 | } 183 | 184 | if (options?.organizationHooks?.beforeCreateOrganization) { 185 | const response = 186 | await options?.organizationHooks.beforeCreateOrganization({ 187 | organization: orgData, 188 | user, 189 | }); 190 | if (response && typeof response === "object" && "data" in response) { 191 | orgData = { 192 | ...ctx.body, 193 | ...response.data, 194 | }; 195 | } 196 | } 197 | 198 | const organization = await adapter.createOrganization({ 199 | organization: { 200 | ...orgData, 201 | createdAt: new Date(), 202 | }, 203 | }); 204 | 205 | let member: 206 | | (Member & InferAdditionalFieldsFromPluginOptions<"member", O, false>) 207 | | undefined; 208 | let teamMember: TeamMember | null = null; 209 | let data = { 210 | userId: user.id, 211 | organizationId: organization.id, 212 | role: ctx.context.orgOptions.creatorRole || "owner", 213 | }; 214 | if (options?.organizationHooks?.beforeAddMember) { 215 | const response = await options?.organizationHooks.beforeAddMember({ 216 | member: { 217 | userId: user.id, 218 | organizationId: organization.id, 219 | role: ctx.context.orgOptions.creatorRole || "owner", 220 | }, 221 | user, 222 | organization, 223 | }); 224 | if (response && typeof response === "object" && "data" in response) { 225 | data = { 226 | ...data, 227 | ...response.data, 228 | }; 229 | } 230 | } 231 | member = await adapter.createMember(data); 232 | if (options?.organizationHooks?.afterAddMember) { 233 | await options?.organizationHooks.afterAddMember({ 234 | member, 235 | user, 236 | organization, 237 | }); 238 | } 239 | if ( 240 | options?.teams?.enabled && 241 | options.teams.defaultTeam?.enabled !== false 242 | ) { 243 | let teamData = { 244 | organizationId: organization.id, 245 | name: `${organization.name}`, 246 | createdAt: new Date(), 247 | }; 248 | if (options?.organizationHooks?.beforeCreateTeam) { 249 | const response = await options?.organizationHooks.beforeCreateTeam({ 250 | team: { 251 | organizationId: organization.id, 252 | name: `${organization.name}`, 253 | }, 254 | user, 255 | organization, 256 | }); 257 | if (response && typeof response === "object" && "data" in response) { 258 | teamData = { 259 | ...teamData, 260 | ...response.data, 261 | }; 262 | } 263 | } 264 | const defaultTeam = 265 | (await options.teams.defaultTeam?.customCreateDefaultTeam?.( 266 | organization, 267 | ctx.request, 268 | )) || (await adapter.createTeam(teamData)); 269 | 270 | teamMember = await adapter.findOrCreateTeamMember({ 271 | teamId: defaultTeam.id, 272 | userId: user.id, 273 | }); 274 | 275 | if (options?.organizationHooks?.afterCreateTeam) { 276 | await options?.organizationHooks.afterCreateTeam({ 277 | team: defaultTeam, 278 | user, 279 | organization, 280 | }); 281 | } 282 | } 283 | 284 | if (options.organizationCreation?.afterCreate) { 285 | await options.organizationCreation.afterCreate( 286 | { 287 | organization, 288 | user, 289 | member, 290 | }, 291 | ctx.request, 292 | ); 293 | } 294 | 295 | if (options?.organizationHooks?.afterCreateOrganization) { 296 | await options?.organizationHooks.afterCreateOrganization({ 297 | organization, 298 | user, 299 | member, 300 | }); 301 | } 302 | 303 | if (ctx.context.session && !ctx.body.keepCurrentActiveOrganization) { 304 | await adapter.setActiveOrganization( 305 | ctx.context.session.session.token, 306 | organization.id, 307 | ctx, 308 | ); 309 | } 310 | 311 | if ( 312 | teamMember && 313 | ctx.context.session && 314 | !ctx.body.keepCurrentActiveOrganization 315 | ) { 316 | await adapter.setActiveTeam( 317 | ctx.context.session.session.token, 318 | teamMember.teamId, 319 | ctx, 320 | ); 321 | } 322 | 323 | return ctx.json({ 324 | ...organization, 325 | metadata: 326 | organization.metadata && typeof organization.metadata === "string" 327 | ? JSON.parse(organization.metadata) 328 | : organization.metadata, 329 | members: [member], 330 | }); 331 | }, 332 | ); 333 | }; 334 | 335 | export const checkOrganizationSlug = <O extends OrganizationOptions>( 336 | options: O, 337 | ) => 338 | createAuthEndpoint( 339 | "/organization/check-slug", 340 | { 341 | method: "POST", 342 | body: z.object({ 343 | slug: z.string().meta({ 344 | description: 'The organization slug to check. Eg: "my-org"', 345 | }), 346 | }), 347 | use: [requestOnlySessionMiddleware, orgMiddleware], 348 | }, 349 | async (ctx) => { 350 | const orgAdapter = getOrgAdapter<O>(ctx.context, options); 351 | const org = await orgAdapter.findOrganizationBySlug(ctx.body.slug); 352 | if (!org) { 353 | return ctx.json({ 354 | status: true, 355 | }); 356 | } 357 | throw new APIError("BAD_REQUEST", { 358 | message: "slug is taken", 359 | }); 360 | }, 361 | ); 362 | 363 | export const updateOrganization = <O extends OrganizationOptions>( 364 | options?: O, 365 | ) => { 366 | const additionalFieldsSchema = toZodSchema({ 367 | fields: options?.schema?.organization?.additionalFields || {}, 368 | isClientSide: true, 369 | }); 370 | type Body = { 371 | data: { 372 | name?: string; 373 | slug?: string; 374 | logo?: string; 375 | metadata?: Record<string, any>; 376 | } & Partial<InferAdditionalFieldsFromPluginOptions<"organization", O>>; 377 | organizationId?: string | undefined; 378 | }; 379 | return createAuthEndpoint( 380 | "/organization/update", 381 | { 382 | method: "POST", 383 | body: z.object({ 384 | data: z 385 | .object({ 386 | ...additionalFieldsSchema.shape, 387 | name: z 388 | .string() 389 | .min(1) 390 | .meta({ 391 | description: "The name of the organization", 392 | }) 393 | .optional(), 394 | slug: z 395 | .string() 396 | .min(1) 397 | .meta({ 398 | description: "The slug of the organization", 399 | }) 400 | .optional(), 401 | logo: z 402 | .string() 403 | .meta({ 404 | description: "The logo of the organization", 405 | }) 406 | .optional(), 407 | metadata: z 408 | .record(z.string(), z.any()) 409 | .meta({ 410 | description: "The metadata of the organization", 411 | }) 412 | .optional(), 413 | }) 414 | .partial(), 415 | organizationId: z 416 | .string() 417 | .meta({ 418 | description: 'The organization ID. Eg: "org-id"', 419 | }) 420 | .optional(), 421 | }), 422 | requireHeaders: true, 423 | use: [orgMiddleware], 424 | metadata: { 425 | $Infer: { 426 | body: {} as Body, 427 | }, 428 | openapi: { 429 | description: "Update an organization", 430 | responses: { 431 | "200": { 432 | description: "Success", 433 | content: { 434 | "application/json": { 435 | schema: { 436 | type: "object", 437 | description: "The updated organization", 438 | $ref: "#/components/schemas/Organization", 439 | }, 440 | }, 441 | }, 442 | }, 443 | }, 444 | }, 445 | }, 446 | }, 447 | async (ctx) => { 448 | const session = await ctx.context.getSession(ctx); 449 | if (!session) { 450 | throw new APIError("UNAUTHORIZED", { 451 | message: "User not found", 452 | }); 453 | } 454 | const organizationId = 455 | ctx.body.organizationId || session.session.activeOrganizationId; 456 | if (!organizationId) { 457 | throw new APIError("BAD_REQUEST", { 458 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 459 | }); 460 | } 461 | const adapter = getOrgAdapter<O>(ctx.context, options); 462 | const member = await adapter.findMemberByOrgId({ 463 | userId: session.user.id, 464 | organizationId: organizationId, 465 | }); 466 | if (!member) { 467 | throw new APIError("BAD_REQUEST", { 468 | message: 469 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 470 | }); 471 | } 472 | const canUpdateOrg = await hasPermission( 473 | { 474 | permissions: { 475 | organization: ["update"], 476 | }, 477 | role: member.role, 478 | options: ctx.context.orgOptions, 479 | organizationId, 480 | }, 481 | ctx, 482 | ); 483 | if (!canUpdateOrg) { 484 | throw new APIError("FORBIDDEN", { 485 | message: 486 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION, 487 | }); 488 | } 489 | if (options?.organizationHooks?.beforeUpdateOrganization) { 490 | const response = 491 | await options.organizationHooks.beforeUpdateOrganization({ 492 | organization: ctx.body.data, 493 | user: session.user, 494 | member, 495 | }); 496 | if (response && typeof response === "object" && "data" in response) { 497 | ctx.body.data = { 498 | ...ctx.body.data, 499 | ...response.data, 500 | }; 501 | } 502 | } 503 | const updatedOrg = await adapter.updateOrganization( 504 | organizationId, 505 | ctx.body.data, 506 | ); 507 | if (options?.organizationHooks?.afterUpdateOrganization) { 508 | await options.organizationHooks.afterUpdateOrganization({ 509 | organization: updatedOrg, 510 | user: session.user, 511 | member, 512 | }); 513 | } 514 | return ctx.json(updatedOrg); 515 | }, 516 | ); 517 | }; 518 | 519 | export const deleteOrganization = <O extends OrganizationOptions>( 520 | options: O, 521 | ) => { 522 | return createAuthEndpoint( 523 | "/organization/delete", 524 | { 525 | method: "POST", 526 | body: z.object({ 527 | organizationId: z.string().meta({ 528 | description: "The organization id to delete", 529 | }), 530 | }), 531 | requireHeaders: true, 532 | use: [orgMiddleware], 533 | metadata: { 534 | openapi: { 535 | description: "Delete an organization", 536 | responses: { 537 | "200": { 538 | description: "Success", 539 | content: { 540 | "application/json": { 541 | schema: { 542 | type: "string", 543 | description: "The organization id that was deleted", 544 | }, 545 | }, 546 | }, 547 | }, 548 | }, 549 | }, 550 | }, 551 | }, 552 | async (ctx) => { 553 | const disableOrganizationDeletion = 554 | ctx.context.orgOptions.organizationDeletion?.disabled || 555 | ctx.context.orgOptions.disableOrganizationDeletion; 556 | if (disableOrganizationDeletion) { 557 | if (ctx.context.orgOptions.organizationDeletion?.disabled) { 558 | ctx.context.logger.info( 559 | "`organizationDeletion.disabled` is deprecated. Use `disableOrganizationDeletion` instead", 560 | ); 561 | } 562 | throw new APIError("NOT_FOUND", { 563 | message: "Organization deletion is disabled", 564 | }); 565 | } 566 | const session = await ctx.context.getSession(ctx); 567 | if (!session) { 568 | throw new APIError("UNAUTHORIZED", { status: 401 }); 569 | } 570 | 571 | const organizationId = ctx.body.organizationId; 572 | if (!organizationId) { 573 | return ctx.json(null, { 574 | status: 400, 575 | body: { 576 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 577 | }, 578 | }); 579 | } 580 | const adapter = getOrgAdapter<O>(ctx.context, options); 581 | const member = await adapter.findMemberByOrgId({ 582 | userId: session.user.id, 583 | organizationId: organizationId, 584 | }); 585 | if (!member) { 586 | throw new APIError("BAD_REQUEST", { 587 | message: 588 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 589 | }); 590 | } 591 | const canDeleteOrg = await hasPermission( 592 | { 593 | role: member.role, 594 | permissions: { 595 | organization: ["delete"], 596 | }, 597 | organizationId, 598 | options: ctx.context.orgOptions, 599 | }, 600 | ctx, 601 | ); 602 | if (!canDeleteOrg) { 603 | throw new APIError("FORBIDDEN", { 604 | message: 605 | ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION, 606 | }); 607 | } 608 | if (organizationId === session.session.activeOrganizationId) { 609 | /** 610 | * If the organization is deleted, we set the active organization to null 611 | */ 612 | await adapter.setActiveOrganization(session.session.token, null, ctx); 613 | } 614 | 615 | const org = await adapter.findOrganizationById(organizationId); 616 | if (!org) { 617 | throw new APIError("BAD_REQUEST"); 618 | } 619 | if (options?.organizationHooks?.beforeDeleteOrganization) { 620 | await options.organizationHooks.beforeDeleteOrganization({ 621 | organization: org, 622 | user: session.user, 623 | }); 624 | } 625 | await adapter.deleteOrganization(organizationId); 626 | if (options?.organizationHooks?.afterDeleteOrganization) { 627 | await options.organizationHooks.afterDeleteOrganization({ 628 | organization: org, 629 | user: session.user, 630 | }); 631 | } 632 | return ctx.json(org); 633 | }, 634 | ); 635 | }; 636 | export const getFullOrganization = <O extends OrganizationOptions>( 637 | options: O, 638 | ) => 639 | createAuthEndpoint( 640 | "/organization/get-full-organization", 641 | { 642 | method: "GET", 643 | query: z.optional( 644 | z.object({ 645 | organizationId: z 646 | .string() 647 | .meta({ 648 | description: "The organization id to get", 649 | }) 650 | .optional(), 651 | organizationSlug: z 652 | .string() 653 | .meta({ 654 | description: "The organization slug to get", 655 | }) 656 | .optional(), 657 | membersLimit: z 658 | .number() 659 | .or(z.string().transform((val) => parseInt(val))) 660 | .meta({ 661 | description: 662 | "The limit of members to get. By default, it uses the membershipLimit option which defaults to 100.", 663 | }) 664 | .optional(), 665 | }), 666 | ), 667 | requireHeaders: true, 668 | use: [orgMiddleware, orgSessionMiddleware], 669 | metadata: { 670 | openapi: { 671 | description: "Get the full organization", 672 | responses: { 673 | "200": { 674 | description: "Success", 675 | content: { 676 | "application/json": { 677 | schema: { 678 | type: "object", 679 | description: "The organization", 680 | $ref: "#/components/schemas/Organization", 681 | }, 682 | }, 683 | }, 684 | }, 685 | }, 686 | }, 687 | }, 688 | }, 689 | async (ctx) => { 690 | const session = ctx.context.session; 691 | const organizationId = 692 | ctx.query?.organizationSlug || 693 | ctx.query?.organizationId || 694 | session.session.activeOrganizationId; 695 | // return null if no organization is found to avoid erroring since this is a usual scenario 696 | if (!organizationId) { 697 | ctx.context.logger.info("No active organization found, returning null"); 698 | return ctx.json(null, { 699 | status: 200, 700 | }); 701 | } 702 | const adapter = getOrgAdapter<O>(ctx.context, options); 703 | const organization = await adapter.findFullOrganization({ 704 | organizationId, 705 | isSlug: !!ctx.query?.organizationSlug, 706 | includeTeams: ctx.context.orgOptions.teams?.enabled, 707 | membersLimit: ctx.query?.membersLimit, 708 | }); 709 | if (!organization) { 710 | throw new APIError("BAD_REQUEST", { 711 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 712 | }); 713 | } 714 | const isMember = await adapter.checkMembership({ 715 | userId: session.user.id, 716 | organizationId: organization.id, 717 | }); 718 | if (!isMember) { 719 | await adapter.setActiveOrganization(session.session.token, null, ctx); 720 | throw new APIError("FORBIDDEN", { 721 | message: 722 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 723 | }); 724 | } 725 | type OrganizationReturn = O["teams"] extends { enabled: true } 726 | ? { 727 | members: InferMember<O>[]; 728 | invitations: InferInvitation<O>[]; 729 | teams: Team[]; 730 | } & InferOrganization<O> 731 | : { 732 | members: InferMember<O>[]; 733 | invitations: InferInvitation<O>[]; 734 | } & InferOrganization<O>; 735 | return ctx.json(organization as unknown as OrganizationReturn); 736 | }, 737 | ); 738 | 739 | export const setActiveOrganization = <O extends OrganizationOptions>( 740 | options: O, 741 | ) => { 742 | return createAuthEndpoint( 743 | "/organization/set-active", 744 | { 745 | method: "POST", 746 | body: z.object({ 747 | organizationId: z 748 | .string() 749 | .meta({ 750 | description: 751 | 'The organization id to set as active. It can be null to unset the active organization. Eg: "org-id"', 752 | }) 753 | .nullable() 754 | .optional(), 755 | organizationSlug: z 756 | .string() 757 | .meta({ 758 | description: 759 | 'The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided. Eg: "org-slug"', 760 | }) 761 | .optional(), 762 | }), 763 | use: [orgSessionMiddleware, orgMiddleware], 764 | metadata: { 765 | openapi: { 766 | description: "Set the active organization", 767 | responses: { 768 | "200": { 769 | description: "Success", 770 | content: { 771 | "application/json": { 772 | schema: { 773 | type: "object", 774 | description: "The organization", 775 | $ref: "#/components/schemas/Organization", 776 | }, 777 | }, 778 | }, 779 | }, 780 | }, 781 | }, 782 | }, 783 | }, 784 | async (ctx) => { 785 | const adapter = getOrgAdapter<O>(ctx.context, options); 786 | const session = ctx.context.session; 787 | let organizationId = ctx.body.organizationId; 788 | let organizationSlug = ctx.body.organizationSlug; 789 | 790 | if (organizationId === null) { 791 | const sessionOrgId = session.session.activeOrganizationId; 792 | if (!sessionOrgId) { 793 | return ctx.json(null); 794 | } 795 | const updatedSession = await adapter.setActiveOrganization( 796 | session.session.token, 797 | null, 798 | ctx, 799 | ); 800 | await setSessionCookie(ctx, { 801 | session: updatedSession, 802 | user: session.user, 803 | }); 804 | return ctx.json(null); 805 | } 806 | 807 | if (!organizationId && !organizationSlug) { 808 | const sessionOrgId = session.session.activeOrganizationId; 809 | if (!sessionOrgId) { 810 | return ctx.json(null); 811 | } 812 | organizationId = sessionOrgId; 813 | } 814 | 815 | if (organizationSlug && !organizationId) { 816 | const organization = 817 | await adapter.findOrganizationBySlug(organizationSlug); 818 | if (!organization) { 819 | throw new APIError("BAD_REQUEST", { 820 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 821 | }); 822 | } 823 | organizationId = organization.id; 824 | } 825 | 826 | if (!organizationId) { 827 | throw new APIError("BAD_REQUEST", { 828 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 829 | }); 830 | } 831 | 832 | const isMember = await adapter.checkMembership({ 833 | userId: session.user.id, 834 | organizationId, 835 | }); 836 | if (!isMember) { 837 | await adapter.setActiveOrganization(session.session.token, null, ctx); 838 | throw new APIError("FORBIDDEN", { 839 | message: 840 | ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, 841 | }); 842 | } 843 | 844 | let organization = await adapter.findOrganizationById(organizationId); 845 | if (!organization) { 846 | throw new APIError("BAD_REQUEST", { 847 | message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, 848 | }); 849 | } 850 | const updatedSession = await adapter.setActiveOrganization( 851 | session.session.token, 852 | organization.id, 853 | ctx, 854 | ); 855 | await setSessionCookie(ctx, { 856 | session: updatedSession, 857 | user: session.user, 858 | }); 859 | type OrganizationReturn = O["teams"] extends { enabled: true } 860 | ? { 861 | members: InferMember<O>[]; 862 | invitations: InferInvitation<O>[]; 863 | teams: Team[]; 864 | } & InferOrganization<O> 865 | : { 866 | members: InferMember<O>[]; 867 | invitations: InferInvitation<O>[]; 868 | } & InferOrganization<O>; 869 | return ctx.json(organization as unknown as OrganizationReturn); 870 | }, 871 | ); 872 | }; 873 | 874 | export const listOrganizations = <O extends OrganizationOptions>(options: O) => 875 | createAuthEndpoint( 876 | "/organization/list", 877 | { 878 | method: "GET", 879 | use: [orgMiddleware, orgSessionMiddleware], 880 | metadata: { 881 | openapi: { 882 | description: "List all organizations", 883 | responses: { 884 | "200": { 885 | description: "Success", 886 | content: { 887 | "application/json": { 888 | schema: { 889 | type: "array", 890 | items: { 891 | $ref: "#/components/schemas/Organization", 892 | }, 893 | }, 894 | }, 895 | }, 896 | }, 897 | }, 898 | }, 899 | }, 900 | }, 901 | async (ctx) => { 902 | const adapter = getOrgAdapter<O>(ctx.context, options); 903 | const organizations = await adapter.listOrganizations( 904 | ctx.context.session.user.id, 905 | ); 906 | return ctx.json(organizations); 907 | }, 908 | ); 909 | ```