#
tokens: 44038/50000 2/1091 files (page 46/49)
lines: off (toggle) GitHub
raw markdown copy
This is page 46 of 49. Use http://codebase.md/better-auth/better-auth?lines=false&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/organization.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, expect, expectTypeOf, it } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { organization } from "./organization";
import { createAuthClient } from "../../client";
import { inferOrgAdditionalFields, organizationClient } from "./client";
import { createAccessControl } from "../access";
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import { APIError, type Prettify } from "better-call";
import { memoryAdapter } from "../../adapters/memory-adapter";
import type { OrganizationOptions } from "./types";
import type { PrettifyDeep } from "../../types/helper";
import type { InvitationStatus } from "./schema";
import { admin } from "../admin";
import { adminAc, defaultStatements, memberAc, ownerAc } from "./access";
import { nextCookies } from "../../integrations/next-js";

describe("organization", async (it) => {
	const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
		await getTestInstance({
			user: {
				modelName: "users",
			},
			plugins: [
				organization({
					membershipLimit: 6,
					async sendInvitationEmail(data, request) {},
					schema: {
						organization: {
							modelName: "team",
						},
						member: {
							modelName: "teamMembers",
							fields: {
								userId: "user_id",
							},
						},
					},
					invitationLimit: 3,
				}),
			],
			logger: {
				level: "error",
			},
		});

	const { headers } = await signInWithTestUser();
	const client = createAuthClient({
		plugins: [organizationClient()],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl: async (url, init) => {
				return auth.handler(new Request(url, init));
			},
		},
	});

	let organizationId: string;
	let organization2Id: string;
	it("create organization", async () => {
		const organization = await client.organization.create({
			name: "test",
			slug: "test",
			metadata: {
				test: "test",
			},
			fetchOptions: {
				headers,
			},
		});
		organizationId = organization.data?.id as string;
		expect(organization.data?.name).toBeDefined();
		expect(organization.data?.metadata).toBeDefined();
		expect(organization.data?.members.length).toBe(1);
		expect(organization.data?.members[0]?.role).toBe("owner");
		const session = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		expect((session.data?.session as any).activeOrganizationId).toBe(
			organizationId,
		);
	});
	it("should check if organization slug is available", async () => {
		const { headers } = await signInWithTestUser();

		const unusedSlug = await client.organization.checkSlug({
			slug: "unused-slug",
			fetchOptions: {
				headers,
			},
		});
		expect(unusedSlug.data?.status).toBe(true);

		const existingSlug = await client.organization.checkSlug({
			slug: "test",
			fetchOptions: {
				headers,
			},
		});
		expect(existingSlug.error?.status).toBe(400);
		expect(existingSlug.error?.message).toBe("slug is taken");
	});

	it("should prevent creating organization with empty slug", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await client.organization.create({
			name: "test-empty-slug",
			slug: "",
			fetchOptions: {
				headers,
			},
		});
		expect(organization.error?.status).toBe(400);
	});

	it("should prevent creating organization with empty name", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await client.organization.create({
			name: "",
			slug: "test-empty-name",
			fetchOptions: {
				headers,
			},
		});
		expect(organization.error?.status).toBe(400);
	});

	it("should create organization directly in the server without cookie", async () => {
		const session = await client.getSession({
			fetchOptions: {
				headers,
			},
		});

		const organization = await auth.api.createOrganization({
			body: {
				name: "test2",
				slug: "test2",
				userId: session.data?.session.userId,
			},
		});

		organization2Id = organization?.id as string;
		expect(organization?.name).toBe("test2");
		expect(organization?.members.length).toBe(1);
		expect(organization?.members[0]?.role).toBe("owner");
	});
	it("should allow listing organizations", async () => {
		const organizations = await client.organization.list({
			fetchOptions: {
				headers,
			},
		});
		expect(organizations.data?.length).toBe(2);
	});

	it("should allow updating organization", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await client.organization.update({
			organizationId,
			data: {
				name: "test2",
			},
			fetchOptions: {
				headers,
			},
		});
		expect(organization.data?.name).toBe("test2");
	});

	it("should allow updating organization metadata", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await client.organization.update({
			organizationId,
			data: {
				metadata: {
					test: "test2",
				},
			},
			fetchOptions: {
				headers,
			},
		});
		expect(organization.data?.metadata?.test).toBe("test2");
	});

	it("should prevent updating organization to empty slug", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await client.organization.update({
			organizationId,
			data: {
				slug: "",
			},
			fetchOptions: {
				headers,
			},
		});
		expect(organization.error?.status).toBe(400);
	});

	it("should prevent updating organization to empty name", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await client.organization.update({
			organizationId,
			data: {
				name: "",
			},
			fetchOptions: {
				headers,
			},
		});
		expect(organization.error?.status).toBe(400);
	});

	it("should allow activating organization and set session", async () => {
		const organization = await client.organization.setActive({
			organizationId,
			fetchOptions: {
				headers,
			},
		});

		expect(organization.data?.id).toBe(organizationId);
		const session = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		expect((session.data?.session as any).activeOrganizationId).toBe(
			organizationId,
		);
	});
	it("should allow activating organization by slug", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await client.organization.setActive({
			organizationSlug: "test2",
			fetchOptions: {
				headers,
			},
		});
		const session = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		expect((session.data?.session as any).activeOrganizationId).toBe(
			organization2Id,
		);
	});

	it("should allow getting full org on server", async () => {
		const org = await auth.api.getFullOrganization({
			headers,
		});
		expect(org?.members.length).toBe(1);
	});

	it("should allow getting full org on server using slug", async () => {
		const org = await auth.api.getFullOrganization({
			headers,
			query: {
				organizationSlug: "test",
			},
		});
		expect(org?.members.length).toBe(1);
	});

	it.each([
		{
			role: "owner",
			newUser: {
				email: "[email protected]",
				password: "test123456",
				name: "test2",
			},
		},
		{
			role: "admin",
			newUser: {
				email: "[email protected]",
				password: "test123456",
				name: "test3",
			},
		},
		{
			role: "member",
			newUser: {
				email: "[email protected]",
				password: "test123456",
				name: "test4",
			},
		},
	])("invites user to organization with role", async ({ role, newUser }) => {
		const { headers } = await signInWithTestUser();
		const invite = await client.organization.inviteMember({
			organizationId: organizationId,
			email: newUser.email,
			role: role as "owner",
			fetchOptions: {
				headers,
			},
		});
		if (!invite.data) throw new Error("Invitation not created");
		expect(invite.data.email).toBe(newUser.email);
		expect(invite.data.role).toBe(role);
		await client.signUp.email({
			email: newUser.email,
			password: newUser.password,
			name: newUser.name,
		});
		const { headers: headers2 } = await signInWithUser(
			newUser.email,
			newUser.password,
		);

		const wrongInvitation = await client.organization.acceptInvitation({
			invitationId: "123",
			fetchOptions: {
				headers: headers2,
			},
		});
		expect(wrongInvitation.error?.status).toBe(400);

		const wrongPerson = await client.organization.acceptInvitation({
			invitationId: invite.data!.id!,
			fetchOptions: {
				headers,
			},
		});
		expect(wrongPerson.error?.status).toBe(403);

		const invitation = await client.organization.acceptInvitation({
			invitationId: invite.data!.id!,
			fetchOptions: {
				headers: headers2,
			},
		});
		expect(invitation.data?.invitation.status).toBe("accepted");
		const invitedUserSession = await client.getSession({
			fetchOptions: {
				headers: headers2,
			},
		});
		expect((invitedUserSession.data?.session as any).activeOrganizationId).toBe(
			organizationId,
		);
	});

	it("should create invitation with multiple roles", async () => {
		const invite = await client.organization.inviteMember({
			organizationId: organizationId,
			email: "[email protected]",
			role: ["admin", "member"],
			fetchOptions: {
				headers,
			},
		});
		expect(invite.data?.role).toBe("admin,member");
	});

	it("should not allow inviting a user twice regardless of email casing", async () => {
		const rng = crypto.randomUUID();
		const user = {
			email: `${rng}@email.com`,
			password: rng,
			name: rng,
		};
		const { headers } = await signInWithTestUser();

		const invite = await client.organization.inviteMember({
			organizationId,
			email: user.email,
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		if (!invite.data) throw new Error("Invitation not created");
		expect(invite.data.createdAt).toBeInstanceOf(Date);
		expect(invite.data.email).toBe(user.email);

		const inviteAgain = await client.organization.inviteMember({
			organizationId,
			email: user.email,
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		expect(inviteAgain.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
		);

		const inviteAgainUpper = await client.organization.inviteMember({
			organizationId,
			email: user.email.toUpperCase(),
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		expect(inviteAgainUpper.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
		);

		await client.signUp.email({
			email: user.email,
			password: user.password,
			name: user.name,
		});
		const { headers: userHeaders } = await signInWithUser(
			user.email,
			user.password,
		);
		const acceptRes = await client.organization.acceptInvitation({
			invitationId: invite.data!.id!,
			fetchOptions: {
				headers: userHeaders,
			},
		});
		expect(acceptRes.data?.invitation.status).toBe("accepted");

		const inviteMemberAgain = await client.organization.inviteMember({
			organizationId,
			email: user.email,
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		expect(inviteMemberAgain.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
		);

		const inviteMemberAgainUpper = await client.organization.inviteMember({
			organizationId,
			email: user.email.toUpperCase(),
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		expect(inviteMemberAgainUpper.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
		);
	});

	it("should allow getting a member", async () => {
		const { headers } = await signInWithTestUser();
		await client.organization.setActive({
			organizationId,
			fetchOptions: {
				headers,
			},
		});
		const member = await client.organization.getActiveMember({
			fetchOptions: {
				headers,
			},
		});
		expect(member.data).toMatchObject({
			role: "owner",
		});
	});

	it("should allow updating member", async () => {
		const { headers, user } = await signInWithTestUser();
		const org = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers,
			},
		});
		if (!org.data) throw new Error("Organization not found");
		expect(org.data.members[3]!.role).toBe("member");
		const member = await client.organization.updateMemberRole({
			organizationId: org.data!.id,
			memberId: org.data!.members[3]!.id,
			role: "admin",
			fetchOptions: {
				headers,
			},
		});
		expect(member.data?.role).toBe("admin");
	});

	it("should allow setting multiple roles", async () => {
		const { headers } = await signInWithTestUser();
		const org = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers,
			},
		});
		const c = await client.organization.updateMemberRole({
			organizationId: org.data!.id,
			role: ["member", "admin"],
			memberId: org.data!.members[1]!.id,
			fetchOptions: {
				headers,
			},
		});
		expect(c.data?.role).toBe("member,admin");
	});

	it("should allow setting multiple roles when you have multiple yourself", async () => {
		const { headers, user } = await signInWithTestUser();
		const org = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers,
			},
		});

		const activeMember = org?.data?.members.find((m) => m.userId === user.id);

		expect(activeMember?.role).toBe("owner");

		const c1 = await client.organization.updateMemberRole({
			organizationId: org.data?.id as string,
			role: ["owner", "admin"],
			memberId: activeMember?.id as string,
			fetchOptions: {
				headers,
			},
		});

		expect(c1.data?.role).toBe("owner,admin");

		const c2 = await client.organization.updateMemberRole({
			organizationId: org.data?.id as string,
			role: ["owner"],
			memberId: activeMember!.id as string,
			fetchOptions: {
				headers,
			},
		});

		expect(c2.data?.role).toBe("owner");
	});

	const adminUser = {
		email: "[email protected]",
		password: "test123456",
		name: "test3",
	};

	it("should not allow inviting member with a creator role unless they are creator", async () => {
		const { headers } = await signInWithUser(
			adminUser.email,
			adminUser.password,
		);
		const invite = await client.organization.inviteMember({
			organizationId: organizationId,
			email: adminUser.email,
			role: "owner",
			fetchOptions: {
				headers,
			},
		});
		expect(invite.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE,
		);
	});

	it("should allow leaving organization", async () => {
		const newUser = {
			email: "[email protected]",
			name: "leaving member",
			password: "password",
		};
		const headers = new Headers();
		const res = await client.signUp.email(newUser, {
			onSuccess: cookieSetter(headers),
		});
		const member = await auth.api.addMember({
			body: {
				organizationId,
				userId: res.data?.user.id!,
				role: "admin",
			},
		});
		const leaveRes = await client.organization.leave(
			{
				organizationId,
			},
			{
				headers,
			},
		);
		expect(leaveRes.data).toMatchObject({
			userId: res.data?.user.id!,
		});
	});

	it("shouldn't allow updating owner role if you're not owner", async () => {
		const { headers } = await signInWithTestUser();
		const { members } = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers,
				throw: true,
			},
		});
		const { headers: adminHeaders } = await signInWithUser(
			adminUser.email,
			adminUser.password,
		);

		const res = await client.organization.updateMemberRole({
			organizationId: organizationId,
			role: "admin",
			memberId: members.find((m) => m.role === "owner")?.id!,
			fetchOptions: {
				headers: adminHeaders,
			},
		});
		expect(res.error?.status).toBe(403);
	});

	it("should allow removing member from organization", async () => {
		const { headers } = await signInWithTestUser();
		const orgBefore = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers,
			},
		});

		expect(orgBefore.data?.members.length).toBe(5);
		await client.organization.removeMember({
			organizationId: organizationId,
			memberIdOrEmail: adminUser.email,
			fetchOptions: {
				headers,
			},
		});
		const org = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers,
			},
		});
		expect(org.data?.members.length).toBe(4);
	});

	it("shouldn't allow removing last owner from organization", async () => {
		const { headers } = await signInWithTestUser();
		const org = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers,
			},
		});

		if (!org.data) throw new Error("Organization not found");
		const removedOwner = await client.organization.removeMember({
			organizationId: org.data.id,
			memberIdOrEmail: org.data?.members.find((m) => m.role === "owner")!.id,
			fetchOptions: {
				headers,
			},
		});
		expect(removedOwner.error?.status).toBe(400);

		const res = await client.organization.updateMemberRole({
			organizationId: organizationId,
			role: ["owner", "admin"],
			memberId: org.data?.members.find((m) => m.role === "owner")?.id!,
			fetchOptions: {
				headers,
			},
		});

		const removedMultipleRoleOwner = await client.organization.removeMember({
			organizationId: org.data.id,
			memberIdOrEmail: org.data?.members.find((m) => m.role === "owner")!.id,
			fetchOptions: {
				headers,
			},
		});
		expect(removedMultipleRoleOwner.error?.status).toBe(400);
	});

	it("should validate permissions", async () => {
		await client.organization.setActive({
			organizationId,
			fetchOptions: {
				headers,
			},
		});
		const hasPermission = await client.organization.hasPermission({
			permissions: {
				member: ["update"],
			},
			fetchOptions: {
				headers,
			},
		});
		expect(hasPermission.data?.success).toBe(true);

		const hasMultiplePermissions = await client.organization.hasPermission({
			permissions: {
				member: ["update"],
				invitation: ["create"],
			},
			fetchOptions: {
				headers,
			},
		});
		expect(hasMultiplePermissions.data?.success).toBe(true);
	});

	it("should return BAD_REQUEST when non-member tries to delete organization", async () => {
		// Create an organization first
		const testOrg = await client.organization.create({
			name: "test-delete-org",
			slug: "test-delete-org",
			fetchOptions: {
				headers,
			},
		});

		// Create a new user who is not a member of any organization
		const nonMemberUser = {
			email: "[email protected]",
			password: "password123",
			name: "Non Member User",
		};

		await client.signUp.email(nonMemberUser);
		const { headers: nonMemberHeaders } = await signInWithUser(
			nonMemberUser.email,
			nonMemberUser.password,
		);

		// Try to delete an organization they're not a member of
		const deleteResult = await client.organization.delete({
			organizationId: testOrg.data?.id as string,
			fetchOptions: {
				headers: nonMemberHeaders,
			},
		});

		expect(deleteResult.error?.status).toBe(400);
		expect(deleteResult.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
		);
	});

	it("should allow deleting organization", async () => {
		const { headers: adminHeaders } = await signInWithUser(
			adminUser.email,
			adminUser.password,
		);

		const r = await client.organization.delete({
			organizationId,
			fetchOptions: {
				headers: adminHeaders,
			},
		});
		const org = await client.organization.getFullOrganization({
			query: {
				organizationId,
			},
			fetchOptions: {
				headers: adminHeaders,
			},
		});
		expect(org.error?.status).toBe(403);
	});

	it("should have server side methods", async () => {
		expectTypeOf(auth.api.createOrganization).toBeFunction();
		expectTypeOf(auth.api.getInvitation).toBeFunction();
	});

	it("should add member on the server directly", async () => {
		const newUser = await auth.api.signUpEmail({
			body: {
				email: "[email protected]",
				password: "password",
				name: "new member",
			},
		});
		const session = await auth.api.getSession({
			headers: new Headers({
				Authorization: `Bearer ${newUser?.token}`,
			}),
		});
		const org = await auth.api.createOrganization({
			body: {
				name: "test2",
				slug: "test3",
			},
			headers,
		});
		const member = await auth.api.addMember({
			body: {
				organizationId: org?.id,
				userId: session?.user.id!,
				role: "admin",
			},
		});
		expect(member?.role).toBe("admin");
	});

	it("should add member on the server with multiple roles", async () => {
		const newUser = await auth.api.signUpEmail({
			body: {
				email: "[email protected]",
				password: "password",
				name: "new member mr",
			},
		});
		const session = await auth.api.getSession({
			headers: new Headers({
				Authorization: `Bearer ${newUser?.token}`,
			}),
		});
		const org = await auth.api.createOrganization({
			body: {
				name: "test2",
				slug: "test4",
			},
			headers,
		});
		const member = await auth.api.addMember({
			body: {
				organizationId: org?.id,
				userId: session?.user.id!,
				role: ["admin", "member"],
			},
		});
		expect(member?.role).toBe("admin,member");
	});

	it("should respect membershipLimit when adding members to organization", async () => {
		const org = await auth.api.createOrganization({
			body: {
				name: "test-5-membership-limit",
				slug: "test-5-membership-limit",
			},
			headers,
		});

		const users = [
			"[email protected]",
			"[email protected]",
			"[email protected]",
			"[email protected]",
		];

		for (const user of users) {
			const newUser = await auth.api.signUpEmail({
				body: {
					email: user,
					password: "password",
					name: user,
				},
			});
			const session = await auth.api.getSession({
				headers: new Headers({
					Authorization: `Bearer ${newUser?.token}`,
				}),
			});
			await auth.api.addMember({
				body: {
					organizationId: org?.id,
					userId: session?.user.id!,
					role: "admin",
				},
			});
		}

		const userOverLimit = {
			email: "[email protected]",
			password: "password",
			name: "name",
		};
		const userOverLimit2 = {
			email: "[email protected]",
			password: "password",
			name: "name",
		};

		// test API method
		const newUser = await auth.api.signUpEmail({
			body: {
				email: userOverLimit.email,
				password: userOverLimit.password,
				name: userOverLimit.name,
			},
		});
		const session = await auth.api.getSession({
			headers: new Headers({
				Authorization: `Bearer ${newUser?.token}`,
			}),
		});
		await auth.api
			.addMember({
				body: {
					organizationId: org?.id,
					userId: session?.user.id!,
					role: "admin",
				},
			})
			.catch((e: APIError) => {
				expect(e).not.toBeNull();
				expect(e).toBeInstanceOf(APIError);
				expect(e.message).toBe(
					ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
				);
			});
		const invite = await client.organization.inviteMember({
			organizationId: org?.id,
			email: userOverLimit2.email,
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		if (!invite.data) throw new Error("Invitation not created");
		await client.signUp.email({
			email: userOverLimit.email,
			password: userOverLimit.password,
			name: userOverLimit.name,
		});
		const { headers: headers2 } = await signInWithUser(
			userOverLimit2.email,
			userOverLimit2.password,
		);

		await client.signUp.email(
			{
				email: userOverLimit2.email,
				password: userOverLimit2.password,
				name: userOverLimit2.name,
			},
			{
				onSuccess: cookieSetter(headers2),
			},
		);

		const invitation = await client.organization.acceptInvitation({
			invitationId: invite.data!.id!,
			fetchOptions: {
				headers: headers2,
			},
		});
		expect(invitation.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
		);

		const getFullOrganization = await client.organization.getFullOrganization({
			query: {
				organizationId: org?.id,
			},
			fetchOptions: {
				headers,
			},
		});
		expect(getFullOrganization.data?.members.length).toBe(6);
	});

	it("should allow listing invitations for an org", async () => {
		const invitations = await client.organization.listInvitations({
			query: {
				organizationId: organizationId,
			},
			fetchOptions: {
				headers: headers,
			},
		});
		expect(invitations.data?.length).toBe(5);
	});

	it("should allow listing invitations for a user using authClient", async () => {
		const rng = crypto.randomUUID();
		const user = {
			email: `${rng}@email.com`,
			password: rng,
			name: rng,
		};
		const rng2 = crypto.randomUUID();
		const orgAdminUser = {
			email: `${rng2}@email.com`,
			password: rng2,
			name: rng2,
		};
		await auth.api.signUpEmail({
			body: user,
		});
		await auth.api.signUpEmail({
			body: orgAdminUser,
		});
		const { headers: headers2, res: session } = await signInWithUser(
			user.email,
			user.password,
		);
		const { headers: adminHeaders, res: adminSession } = await signInWithUser(
			orgAdminUser.email,
			orgAdminUser.password,
		);
		const orgRng = crypto.randomUUID();
		const org = await auth.api.createOrganization({
			body: {
				name: orgRng,
				slug: orgRng,
			},
			headers: adminHeaders,
		});
		const invitation = await client.organization.inviteMember({
			organizationId: org?.id,
			email: user.email,
			role: "member",
			fetchOptions: {
				headers: adminHeaders,
			},
		});
		const userInvitations = await client.organization.listUserInvitations({
			fetchOptions: {
				headers: headers2,
			},
		});
		expect(userInvitations.data?.[0]!.id).toBe(invitation.data?.id);
		expect(userInvitations.data?.length).toBe(1);
	});

	it("should allow listing invitations for a user using server", async () => {
		const orgInvitations = await client.organization.listInvitations({
			fetchOptions: {
				headers,
			},
		});

		if (!orgInvitations.data?.[0]!.email) throw new Error("No email found");

		const invitations = await auth.api.listUserInvitations({
			query: {
				email: orgInvitations.data?.[0]!.email,
			},
		});

		expect(invitations?.length).toBe(
			orgInvitations.data!.filter(
				(x) => x.email === orgInvitations.data?.[0]!.email,
			).length,
		);

		const invitationsUpper = await auth.api.listUserInvitations({
			query: {
				email: orgInvitations.data?.[0]!.email.toUpperCase(),
			},
		});

		expect(invitationsUpper?.length).toBe(
			orgInvitations.data!.filter(
				(x) => x.email === orgInvitations.data?.[0]!.email,
			).length,
		);
	});
});

describe("access control", async (it) => {
	const ac = createAccessControl({
		project: ["create", "read", "update", "delete"],
		sales: ["create", "read", "update", "delete"],
		...defaultStatements,
	});
	const owner = ac.newRole({
		project: ["create", "delete", "update", "read"],
		sales: ["create", "read", "update", "delete"],
		...ownerAc.statements,
	});
	const admin = ac.newRole({
		project: ["create", "read"],
		sales: ["create", "read"],
		...adminAc.statements,
	});
	const member = ac.newRole({
		project: ["read"],
		sales: ["read"],
		...memberAc.statements,
	});
	const { auth, customFetchImpl, sessionSetter, signInWithTestUser } =
		await getTestInstance({
			plugins: [
				organization({
					ac,
					roles: {
						admin,
						member,
						owner,
					},
					dynamicAccessControl: {
						enabled: true,
					},
				}),
			],
		});

	const authClient = createAuthClient({
		baseURL: "http://localhost:3000",
		plugins: [
			organizationClient({
				ac,
				roles: {
					admin,
					member,
					owner,
				},
				dynamicAccessControl: {
					enabled: true,
				},
			}),
		],
		fetchOptions: {
			customFetchImpl,
		},
	});
	const {
		organization: { checkRolePermission, hasPermission, create },
	} = authClient;

	const { headers, user, session } = await signInWithTestUser();

	const org = await create(
		{
			name: "test",
			slug: "test",
			metadata: {
				test: "test",
			},
		},
		{
			onSuccess: sessionSetter(headers),
			headers,
		},
	);
	if (!org.data) throw new Error("Organization not created");

	it("should return success", async () => {
		const canCreateProject = await checkRolePermission({
			role: "admin",
			permissions: {
				project: ["create"],
			},
		});
		expect(canCreateProject).toBe(true);

		// To be removed when `permission` will be removed entirely
		const canCreateProjectLegacy = await checkRolePermission({
			role: "admin",
			permission: {
				project: ["create"],
			},
		});
		expect(canCreateProjectLegacy).toBe(true);

		const canCreateProjectServer = await hasPermission({
			permissions: {
				project: ["create"],
			},
			fetchOptions: {
				headers,
			},
		});
		expect(canCreateProjectServer.data?.success).toBe(true);
	});

	it("should return not success", async () => {
		const canCreateProject = await checkRolePermission({
			role: "admin",
			permissions: {
				project: ["delete"],
			},
		});
		expect(canCreateProject).toBe(false);
	});

	it("should return not success", async () => {
		const res = await checkRolePermission({
			role: "admin",
			permissions: {
				project: ["read"],
				sales: ["delete"],
			},
		});
		expect(res).toBe(false);
	});
});

describe("invitation limit", async () => {
	const { customFetchImpl, signInWithTestUser } = await getTestInstance({
		plugins: [
			organization({
				invitationLimit: 1,
				async sendInvitationEmail(data, request) {},
			}),
		],
	});
	const client = createAuthClient({
		plugins: [organizationClient()],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl,
		},
	});
	const { headers } = await signInWithTestUser();
	const org = await client.organization.create(
		{
			name: "test",
			slug: "test",
		},
		{
			headers,
		},
	);

	it("should invite member to organization", async () => {
		const invite = await client.organization.inviteMember({
			organizationId: org.data?.id as string,
			email: "[email protected]",
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		expect(invite.data?.status).toBe("pending");
	});

	it("should throw error when invitation limit is reached", async () => {
		const invite = await client.organization.inviteMember({
			organizationId: org.data?.id as string,
			email: "[email protected]",
			role: "member",
			fetchOptions: {
				headers,
			},
		});
		expect(invite.error?.status).toBe(403);
		expect(invite.error?.message).toBe(
			ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED,
		);
	});

	it("should throw error with custom invitation limit", async () => {
		const { auth, signInWithTestUser } = await getTestInstance({
			plugins: [
				organization({
					invitationLimit: async (data, ctx) => {
						return 0;
					},
				}),
			],
		});
		const { headers } = await signInWithTestUser();
		const org = await auth.api.createOrganization({
			body: {
				name: "test",
				slug: "test",
			},
			headers,
		});
		await auth.api
			.createInvitation({
				body: {
					email: "[email protected]",
					role: "member",
					organizationId: org?.id as string,
				},
				headers,
			})
			.catch((e: APIError) => {
				expect(e.message).toBe(
					ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED,
				);
			});
	});
});

describe("cancel pending invitations on re-invite", async () => {
	const { customFetchImpl, signInWithTestUser } = await getTestInstance({
		plugins: [
			organization({
				cancelPendingInvitationsOnReInvite: true,
			}),
		],
	});
	const client = createAuthClient({
		plugins: [organizationClient()],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl,
		},
	});
	const { headers } = await signInWithTestUser();
	const org = await client.organization.create(
		{
			name: "test",
			slug: "test",
		},
		{
			headers,
		},
	);

	it("should cancel pending invitations on re-invite", async () => {
		const invite = await client.organization.inviteMember(
			{
				organizationId: org.data?.id as string,
				email: "[email protected]",
				role: "member",
			},
			{
				headers,
			},
		);
		expect(invite.data?.status).toBe("pending");
		const invite2 = await client.organization.inviteMember(
			{
				organizationId: org.data?.id as string,
				email: "[email protected]",
				role: "member",
				resend: true,
			},
			{
				headers,
			},
		);
		expect(invite2.data?.status).toBe("pending");
		const listInvitations = await client.organization.listInvitations({
			fetchOptions: {
				headers,
			},
		});
		expect(
			listInvitations.data?.filter((invite) => invite.status === "pending")
				.length,
		).toBe(1);
	});
});

describe("resend invitation should reuse existing", async () => {
	const { customFetchImpl, signInWithTestUser } = await getTestInstance({
		plugins: [
			organization({
				async sendInvitationEmail(data, request) {},
			}),
		],
	});
	const client = createAuthClient({
		plugins: [organizationClient()],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl,
		},
	});
	const { headers } = await signInWithTestUser();
	const org = await client.organization.create(
		{
			name: "test",
			slug: "test",
		},
		{
			headers,
		},
	);

	it("should reuse existing invitation when resend is true", async () => {
		const invite = await client.organization.inviteMember(
			{
				organizationId: org.data?.id as string,
				email: "[email protected]",
				role: "member",
			},
			{
				headers,
			},
		);
		expect(invite.data?.status).toBe("pending");
		const originalInviteId = invite.data?.id;

		const invite2 = await client.organization.inviteMember(
			{
				organizationId: org.data?.id as string,
				email: "[email protected]",
				role: "member",
				resend: true,
			},
			{
				headers,
			},
		);
		expect(invite2.data?.status).toBe("pending");
		// Should return the same invitation ID, not create a new one
		expect(invite2.data?.id).toBe(originalInviteId);

		const listInvitations = await client.organization.listInvitations({
			fetchOptions: {
				headers,
			},
		});
		// Should still only have 1 pending invitation, not 2
		expect(
			listInvitations.data?.filter((invite) => invite.status === "pending")
				.length,
		).toBe(1);
	});
});

describe("owner can update roles", async () => {
	const statement = {
		custom: ["custom"],
	} as const;

	const ac = createAccessControl(statement);

	const custom = ac.newRole({
		custom: ["custom"],
	});

	const { auth } = await getTestInstance({
		emailAndPassword: {
			enabled: true,
		},
		plugins: [
			admin(),
			organization({
				ac,
				roles: {
					custom,
					owner: ownerAc,
				},
			}),
		],
	});

	const adminEmail = "[email protected]";
	const adminPassword = "adminpassword";

	await auth.api.createUser({
		body: {
			email: adminEmail,
			password: adminPassword,
			name: "Admin",
			role: "admin",
		},
	});

	const { headers } = await auth.api.signInEmail({
		returnHeaders: true,
		body: {
			email: adminEmail,
			password: adminPassword,
		},
	});

	const adminCookie = headers.getSetCookie()[0]!;

	const org = await auth.api.createOrganization({
		headers: { cookie: adminCookie },
		body: {
			name: "Org",
			slug: "org",
		},
	});

	if (!org) {
		throw new Error("couldn't create an organization");
	}

	const ownerId = org.members.at(0)?.id;
	if (!ownerId) {
		throw new Error("couldn't get the owner id");
	}

	it("allows setting custom role to a user", async () => {
		const userEmail = "[email protected]";
		const userPassword = "userpassword";

		const { user } = await auth.api.createUser({
			headers: { cookie: adminCookie },
			body: {
				name: "user",
				email: userEmail,
				password: userPassword,
			},
		});

		const addMemberRes = await auth.api.addMember({
			headers: { cookie: adminCookie },
			body: {
				organizationId: org.id,
				userId: user.id,
				role: [],
			},
		});

		if (!addMemberRes) {
			throw new Error("couldn't add user as a member to a repo");
		}

		await auth.api.updateMemberRole({
			headers: { cookie: adminCookie },
			body: {
				organizationId: org.id,
				memberId: addMemberRes.id,
				role: ["custom", "owner"],
			},
		});

		const signInRes = await auth.api.signInEmail({
			returnHeaders: true,
			body: {
				email: userEmail,
				password: userPassword,
			},
		});

		const userCookie = signInRes.headers.getSetCookie()[0]!;

		const permissionRes = await auth.api.hasPermission({
			headers: { cookie: userCookie },
			body: {
				organizationId: org.id,
				permissions: {
					custom: ["custom"],
				},
			},
		});

		expect(permissionRes.success).toBe(true);
		expect(permissionRes.error).toBeNull();
	});

	it("allows org owner to set a custom role for themselves", async () => {
		await auth.api.updateMemberRole({
			headers: { cookie: adminCookie },
			body: {
				organizationId: org.id,
				memberId: ownerId,
				role: ["owner", "custom"],
			},
		});

		const permissionRes = await auth.api.hasPermission({
			headers: { cookie: adminCookie },
			body: {
				organizationId: org.id,
				permissions: {
					custom: ["custom"],
				},
			},
		});

		expect(permissionRes.success).toBe(true);
		expect(permissionRes.error).toBeNull();
	});

	it("allows an org owner to remove their own creator role if not sole owner", async () => {
		await auth.api.updateMemberRole({
			headers: { cookie: adminCookie },
			body: {
				organizationId: org.id,
				memberId: ownerId,
				role: [],
			},
		});
	});

	it("should throw error if sole org owner tries to remove creator role"),
		async () => {
			const userEmail = "[email protected]";
			const userPassword = "userpassword";

			const signInRes = await auth.api.signInEmail({
				returnHeaders: true,
				body: {
					email: userEmail,
					password: userPassword,
				},
			});

			const userCookie = signInRes.headers.getSetCookie()[0]!;

			await auth.api
				.updateMemberRole({
					headers: { cookie: userCookie },
					body: {
						organizationId: org.id,
						memberId: ownerId,
						role: [],
					},
				})
				.catch((e: APIError) => {
					expect(e.message).toBe(
						ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER,
					);
				});
		};
});

describe("types", async (it) => {
	const { auth } = await getTestInstance({
		plugins: [organization({})],
	});

	it("should infer active organization", async () => {
		type ActiveOrganization = typeof auth.$Infer.ActiveOrganization;

		type FullOrganization = Awaited<
			ReturnType<typeof auth.api.getFullOrganization>
		>;
		expectTypeOf<FullOrganization>().toEqualTypeOf<ActiveOrganization>();
	});
});

describe("Additional Fields", async () => {
	const db = {
		users: [],
		sessions: [],
		account: [],
		organization: [],
		invitation: [] as {
			id: string;
			invitationRequiredField: string;
			invitationOptionalField?: string;
		}[],
		member: [] as {
			id: string;
			memberRequiredField: string;
			memberOptionalField?: string;
		}[],
		team: [] as {
			id: string;
			teamRequiredField: string;
			teamOptionalField?: string;
		}[],
		teamMember: [] as {
			id: string;
		}[],
	};

	const orgOptions = {
		teams: {
			enabled: true,
		},
		schema: {
			organization: {
				additionalFields: {
					someRequiredField: {
						type: "string",
						required: true,
					},
					someOptionalField: {
						type: "string",
						required: false,
					},
					someHiddenField: {
						type: "string",
						input: false,
					},
				},
			},
			member: {
				additionalFields: {
					memberRequiredField: {
						type: "string",
						required: true,
					},
					memberOptionalField: {
						type: "string",
					},
				},
			},
			team: {
				additionalFields: {
					teamRequiredField: {
						type: "string",
						required: true,
					},
					teamOptionalField: {
						type: "string",
					},
				},
			},
			invitation: {
				additionalFields: {
					invitationRequiredField: {
						type: "string",
						required: true,
					},
					invitationOptionalField: {
						type: "string",
					},
				},
			},
		},
		invitationLimit: 3,
	} satisfies OrganizationOptions;

	const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
		await getTestInstance({
			database: memoryAdapter(db, {
				debugLogs: false,
			}),
			user: {
				modelName: "users",
			},
			plugins: [organization(orgOptions), nextCookies()],
			logger: {
				level: "error",
			},
		});

	const { headers, user } = await signInWithTestUser();
	const client = createAuthClient({
		plugins: [
			organizationClient({
				schema: inferOrgAdditionalFields<typeof auth>(),
				teams: { enabled: true },
			}),
		],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl: async (url, init) => {
				return auth.handler(new Request(url, init));
			},
		},
	});

	const client2 = createAuthClient({
		plugins: [
			organizationClient({
				schema: inferOrgAdditionalFields<typeof auth>(),
				teams: { enabled: true },
			}),
		],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl: async (url, init) => {
				return auth.handler(new Request(url, init));
			},
		},
	});

	it("Expect team endpoints to still be defined", async () => {
		const teams = client.organization.createTeam;
		expect(teams).toBeDefined();
		expectTypeOf<typeof teams>().not.toEqualTypeOf<undefined>();
	});

	it("Should infer the organization schema", async () => {
		const org = client.organization.create;
		const org2 = client2.organization.create;
		type Params = Omit<Parameters<typeof org>[0], "fetchOptions">;
		type Params2 = Omit<Parameters<typeof org2>[0], "fetchOptions">;
		expect(org).toBeDefined();
		expectTypeOf<Params>().toEqualTypeOf<{
			name: string;
			slug: string;
			logo?: string | undefined;
			userId?: string | undefined;
			metadata?: Record<string, any> | undefined;
			someRequiredField: string;
			someOptionalField?: string | undefined;
			keepCurrentActiveOrganization?: boolean | undefined;
		}>();
		expectTypeOf<Params2>().toEqualTypeOf<{
			name: string;
			slug: string;
			logo?: string | undefined;
			userId?: string | undefined;
			metadata?: Record<string, any> | undefined;
			someRequiredField: string;
			someOptionalField?: string | undefined;
			keepCurrentActiveOrganization?: boolean | undefined;
		}>();
	});

	type ExpectedResult = PrettifyDeep<{
		id: string;
		name: string;
		slug: string;
		createdAt: Date;
		logo?: string | null | undefined;
		metadata: any;
		someRequiredField: string;
		someOptionalField?: string | undefined;
		someHiddenField?: string | undefined;
		members: (
			| ({
					id: string;
					organizationId: string;
					userId: string;
					role: string;
					createdAt: Date;
			  } & {
					memberRequiredField: string;
			  } & {
					memberOptionalField?: string | undefined;
			  })
			| undefined
		)[];
	}> | null;
	let org: NonNullable<ExpectedResult>;
	it("create organization", async () => {
		try {
			const orgRes = await auth.api.createOrganization({
				body: {
					name: "test",
					slug: "test",
					someRequiredField: "hey",
					someOptionalField: "hey",
				},
				headers,
			});

			type Result = PrettifyDeep<typeof orgRes>;
			expectTypeOf<Result>().toEqualTypeOf<ExpectedResult>();
			expect(orgRes).not.toBeNull();
			if (!orgRes) throw new Error("Organization is null");
			org = orgRes;
			expect(org.someRequiredField).toBeDefined();
			expect(org.someRequiredField).toBe("hey");
			expect(org.someOptionalField).toBe("hey");
			expect(org.someHiddenField).toBeUndefined();
			//@ts-expect-error
			expect(db.organization[0]?.someRequiredField).toBe("hey");
		} catch (error) {
			throw error;
		}
	});

	it("update organization", async () => {
		const updatedOrg = await auth.api.updateOrganization({
			body: {
				data: {
					someRequiredField: "hey2",
				},
				organizationId: org.id,
			},
			headers,
		});
		type Result = PrettifyDeep<typeof updatedOrg>;
		expect(updatedOrg?.someRequiredField).toBe("hey2");
		//@ts-expect-error
		expect(db.organization[0]?.someRequiredField).toBe("hey2");
		expectTypeOf<Result>().toEqualTypeOf<{
			id: string;
			name: string;
			slug: string;
			createdAt: Date;
			logo?: string | null | undefined;
			someRequiredField: string;
			someOptionalField?: string | undefined;
			metadata: any;
		} | null>();
	});

	it("add member", async () => {
		const newUser = await auth.api.signUpEmail({
			body: {
				email: "[email protected]",
				password: "password",
				name: "new member",
			},
		});

		const member = await auth.api.addMember({
			body: {
				organizationId: org.id,
				userId: newUser.user.id,
				role: "member",
				memberRequiredField: "hey",
				memberOptionalField: "hey2",
			},
		});
		if (!member) throw new Error("Member is null");
		expect(member?.memberRequiredField).toBe("hey");
		expectTypeOf<typeof member.memberRequiredField>().toEqualTypeOf<string>();
		expect(member?.memberOptionalField).toBe("hey2");
		expectTypeOf<typeof member.memberOptionalField>().toEqualTypeOf<
			string | undefined
		>();
		const row = db.member.find((x) => x.id === member?.id)!;
		expect(row).toBeDefined();
		expect(row.memberRequiredField).toBe("hey");
		expect(row.memberOptionalField).toBe("hey2");
	});

	it("create invitation", async () => {
		const invitation = await auth.api.createInvitation({
			body: {
				email: "[email protected]",
				role: "member",
				invitationRequiredField: "hey",
				invitationOptionalField: "hey2",
				organizationId: org.id,
			},
			headers,
		});

		const invitationWithFields = invitation as any;
		expect(invitationWithFields.invitationRequiredField).toBe("hey");
		expectTypeOf<string>().toEqualTypeOf<string>();
		expect(invitationWithFields.invitationOptionalField).toBe("hey2");
		expectTypeOf<string | undefined>().toEqualTypeOf<string | undefined>();
		const row = db.invitation.find((x) => x.id === invitation?.id)!;
		expect(row).toBeDefined();
		expect(row.invitationRequiredField).toBe("hey");
		expect(row.invitationOptionalField).toBe("hey2");
	});

	it("list invitations", async () => {
		const invitations = await auth.api.listInvitations({
			query: {
				organizationId: org.id,
			},
			headers,
		});

		expect(invitations?.length).toBe(1);
		const invitation = invitations[0]!;
		type ResultInvitation = Prettify<typeof invitation>;
		expectTypeOf<ResultInvitation>().toEqualTypeOf<{
			id: string;
			organizationId: string;
			email: string;
			role: "member" | "admin" | "owner";
			status: InvitationStatus;
			createdAt: Date;
			expiresAt: Date;
			inviterId: string;
			invitationRequiredField: string;
			invitationOptionalField?: string | undefined;
			teamId?: string | undefined;
		}>();
		expect(invitation.invitationRequiredField).toBe("hey");
		expect(invitation.invitationOptionalField).toBe("hey2");
	});

	let team: {
		id: string;
		name: string;
		organizationId: string;
		createdAt: Date;
		updatedAt?: Date | undefined;
		teamRequiredField: string;
		teamOptionalField?: string | undefined;
	} | null = null;
	it("create team", async () => {
		team = await auth.api.createTeam({
			body: {
				name: "test",
				teamRequiredField: "hey",
				teamOptionalField: "hey2",
				organizationId: org.id,
			},
			headers,
		});

		expect(team.teamRequiredField).toBe("hey");
		expect(team.teamOptionalField).toBe("hey2");
		const row = db.team.find((x) => x.id === team?.id)!;
		expect(row).toBeDefined();
		expect(row.teamRequiredField).toBe("hey");
		expect(row.teamOptionalField).toBe("hey2");
	});

	it("update team", async () => {
		if (!team) throw new Error("Team is null");
		const updatedTeam = await auth.api.updateTeam({
			body: {
				teamId: team.id,
				data: {
					teamOptionalField: "hey3",
					teamRequiredField: "hey4",
				},
			},
			headers,
		});

		if (!updatedTeam) throw new Error("Updated team is null");
		expect(updatedTeam?.teamOptionalField).toBe("hey3");
		expect(updatedTeam?.teamRequiredField).toBe("hey4");
		expectTypeOf<
			typeof updatedTeam.teamRequiredField
		>().toEqualTypeOf<string>();
		expectTypeOf<typeof updatedTeam.teamOptionalField>().toEqualTypeOf<
			string | undefined
		>();
		const row = db.team.find((x) => x.id === updatedTeam?.id)!;
		expect(row).toBeDefined();
		expect(row.teamOptionalField).toBe("hey3");
		expect(row.teamRequiredField).toBe("hey4");
	});
});

describe("organization hooks", async (it) => {
	let hooksCalled: string[] = [];

	const { auth, signInWithTestUser } = await getTestInstance({
		plugins: [
			organization({
				organizationHooks: {
					beforeCreateOrganization: async (data) => {
						hooksCalled.push("beforeCreateOrganization");
						return {
							data: {
								...data.organization,
								metadata: { hookCalled: true },
							},
						};
					},
					afterCreateOrganization: async (data) => {
						hooksCalled.push("afterCreateOrganization");
					},
					beforeCreateInvitation: async (data) => {
						hooksCalled.push("beforeCreateInvitation");
					},
					afterCreateInvitation: async (data) => {
						hooksCalled.push("afterCreateInvitation");
					},
					beforeAddMember: async (data) => {
						hooksCalled.push("beforeAddMember");
					},
					afterAddMember: async (data) => {
						hooksCalled.push("afterAddMember");
					},
				},
				async sendInvitationEmail() {},
			}),
		],
	});

	const client = createAuthClient({
		plugins: [organizationClient()],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl: async (url, init) => {
				return auth.handler(new Request(url, init));
			},
		},
	});

	const { headers } = await signInWithTestUser();

	it("should call organization creation hooks", async () => {
		hooksCalled = [];
		const organization = await client.organization.create({
			name: "Test Org with Hooks",
			slug: "test-org-hooks",
			fetchOptions: { headers },
		});

		expect(hooksCalled).toContain("beforeCreateOrganization");
		expect(hooksCalled).toContain("afterCreateOrganization");
		expect(organization.data?.metadata).toEqual({ hookCalled: true });
	});

	it("should call invitation hooks", async () => {
		hooksCalled = [];

		await client.organization.inviteMember({
			email: "[email protected]",
			role: "member",
			fetchOptions: { headers },
		});

		expect(hooksCalled).toContain("beforeCreateInvitation");
		expect(hooksCalled).toContain("afterCreateInvitation");
	});
});

```

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

```typescript
import { describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { apiKey, ERROR_CODES } from ".";
import { apiKeyClient } from "./client";
import type { ApiKey } from "./types";
import { APIError } from "better-call";

describe("api-key", async () => {
	const { client, auth, signInWithTestUser } = await getTestInstance(
		{
			plugins: [
				apiKey({
					enableMetadata: true,
					permissions: {
						defaultPermissions: {
							files: ["read"],
						},
					},
				}),
			],
		},
		{
			clientOptions: {
				plugins: [apiKeyClient()],
			},
		},
	);
	const { headers, user } = await signInWithTestUser();

	// =========================================================================
	// CREATE API KEY
	// =========================================================================

	it("should fail to create API keys from client without headers", async () => {
		const apiKeyFail = await client.apiKey.create();

		expect(apiKeyFail.data).toBeNull();
		expect(apiKeyFail.error).toBeDefined();
		expect(apiKeyFail.error?.status).toEqual(401);
		expect(apiKeyFail.error?.statusText).toEqual("UNAUTHORIZED");
		expect(apiKeyFail.error?.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION);
	});

	let firstApiKey: ApiKey;

	it("should successfully create API keys from client with headers", async () => {
		const apiKey = await client.apiKey.create({}, { headers: headers });
		if (apiKey.data) {
			firstApiKey = apiKey.data;
		}

		expect(apiKey.data).not.toBeNull();
		expect(apiKey.data?.key).toBeDefined();
		expect(apiKey.data?.userId).toEqual(user.id);
		expect(apiKey.data?.name).toBeNull();
		expect(apiKey.data?.prefix).toBeNull();
		expect(apiKey.data?.refillInterval).toBeNull();
		expect(apiKey.data?.refillAmount).toBeNull();
		expect(apiKey.data?.lastRefillAt).toBeNull();
		expect(apiKey.data?.enabled).toEqual(true);
		expect(apiKey.data?.rateLimitTimeWindow).toEqual(86400000);
		expect(apiKey.data?.rateLimitMax).toEqual(10);
		expect(apiKey.data?.requestCount).toEqual(0);
		expect(apiKey.data?.remaining).toBeNull();
		expect(apiKey.data?.lastRequest).toBeNull();
		expect(apiKey.data?.expiresAt).toBeNull();
		expect(apiKey.data?.createdAt).toBeDefined();
		expect(apiKey.data?.updatedAt).toBeDefined();
		expect(apiKey.data?.metadata).toBeNull();
		expect(apiKey.error).toBeNull();
	});

	interface Err {
		body: {
			code: string | undefined;
			message: string | undefined;
		};
		status: string;
		statusCode: string;
	}

	it("should fail to create API Keys from server without headers and userId", async () => {
		let res: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({ body: {} });
			res.data = apiKey;
		} catch (error: any) {
			res.error = error;
		}

		expect(res.data).toBeNull();
		expect(res.error).toBeDefined();
		expect(res.error?.statusCode).toEqual(401);
		expect(res.error?.status).toEqual("UNAUTHORIZED");
		expect(res.error?.body.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION);
	});

	it("should fail to create api keys from the client if user id is provided", async () => {
		const { headers, user } = await signInWithTestUser();
		const response = await client.apiKey.create({
			userId: user.id,
		});
		expect(response.error?.status).toBe(401);
		const newUser = await auth.api.signUpEmail({
			body: {
				email: "[email protected]",
				password: "password",
				name: "test-name",
			},
		});
		const response2 = await client.apiKey.create(
			{
				userId: newUser.user.id,
			},
			{
				headers,
			},
		);
		expect(response2.error?.status).toBe(401);
	});

	it("should successfully create API keys from server with userId", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.key).toBeDefined();
		expect(apiKey.userId).toEqual(user.id);
		expect(apiKey.name).toBeNull();
		expect(apiKey.prefix).toBeNull();
		expect(apiKey.refillInterval).toBeNull();
		expect(apiKey.refillAmount).toBeNull();
		expect(apiKey.lastRefillAt).toBeNull();
		expect(apiKey.enabled).toEqual(true);
		expect(apiKey.rateLimitTimeWindow).toEqual(86400000);
		expect(apiKey.rateLimitMax).toEqual(10);
		expect(apiKey.requestCount).toEqual(0);
		expect(apiKey.remaining).toBeNull();
		expect(apiKey.lastRequest).toBeNull();
		expect(apiKey.rateLimitEnabled).toBe(true);
	});

	it("should have the real value from rateLimitEnabled", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
				rateLimitEnabled: false,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.rateLimitEnabled).toBe(false);
	});

	it("should have true if the rate limit is undefined", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
				rateLimitEnabled: undefined,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.rateLimitEnabled).toBe(true);
	});

	it("should require name in API keys if configured", async () => {
		const { auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						requireName: true,
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);

		const { user } = await signInWithTestUser();
		let err: any;
		try {
			await auth.api.createApiKey({
				body: {
					userId: user.id,
				},
			});
		} catch (error) {
			err = error;
		}
		expect(err).toBeDefined();
		expect(err.body.message).toBe(ERROR_CODES.NAME_REQUIRED);
	});

	it("should respect rateLimit configuration from plugin options", async () => {
		const { auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						rateLimit: {
							enabled: false,
							timeWindow: 1000,
							maxRequests: 10,
						},
						enableMetadata: true,
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);

		const { user } = await signInWithTestUser();
		const apiKeyResult = await auth.api.createApiKey({
			body: {
				userId: user.id,
			},
		});

		expect(apiKeyResult).not.toBeNull();
		expect(apiKeyResult.rateLimitEnabled).toBe(false);
		expect(apiKeyResult.rateLimitTimeWindow).toBe(1000);
		expect(apiKeyResult.rateLimitMax).toBe(10);
	});

	it("should create the API key with the given name", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				name: "test-api-key",
			},
			headers,
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.name).toEqual("test-api-key");
	});

	it("should create the API key with a name that's shorter than the allowed minimum", async () => {
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					name: "test-api-key-that-is-shorter-than-the-allowed-minimum",
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
	});

	it("should create the API key with a name that's longer than the allowed maximum", async () => {
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					name: "test-api-key-that-is-longer-than-the-allowed-maximum",
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
	});

	it("should create the API key with the given prefix", async () => {
		const prefix = "test-api-key_";
		const apiKey = await auth.api.createApiKey({
			body: {
				prefix: prefix,
			},
			headers,
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.prefix).toEqual(prefix);
		expect(apiKey.key.startsWith(prefix)).toEqual(true);
	});

	it("should create the API key with a prefix that's shorter than the allowed minimum", async () => {
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					prefix: "test-api-key-that-is-shorter-than-the-allowed-minimum",
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.INVALID_PREFIX_LENGTH,
		);
	});

	it("should create the API key with a prefix that's longer than the allowed maximum", async () => {
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					prefix: "test-api-key-that-is-longer-than-the-allowed-maximum",
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.INVALID_PREFIX_LENGTH,
		);
	});

	it("should create an API key with a custom expiresIn", async () => {
		const expiresIn = 60 * 60 * 24 * 7; // 7 days
		const expectedResult = new Date().getTime() + expiresIn;
		const apiKey = await auth.api.createApiKey({
			body: {
				expiresIn: expiresIn,
			},
			headers,
		});
		expect(apiKey).not.toBeNull();
		expect(apiKey.expiresAt).toBeDefined();
		expect(apiKey.expiresAt?.getTime()).toBeGreaterThanOrEqual(expectedResult);
	});

	it("should support disabling key hashing", async () => {
		const { auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						disableKeyHashing: true,
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const apiKey2 = await auth.api.createApiKey({
			body: {},
			headers,
		});
		const res = await (await auth.$context).adapter.findOne<ApiKey>({
			model: "apikey",
			where: [
				{
					field: "id",
					value: apiKey2.id,
				},
			],
		});
		expect(res?.key).toEqual(apiKey2.key);
	});

	it("should be able to verify with key hashing disabled", async () => {
		const { auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						disableKeyHashing: true,
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const apiKey2 = await auth.api.createApiKey({
			body: {},
			headers,
		});

		const result = await auth.api.verifyApiKey({ body: { key: apiKey2.key } });
		expect(result.valid).toEqual(true);
	});

	it("should fail to create a key with a custom expiresIn value when customExpiresTime is disabled", async () => {
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						enableMetadata: true,
						keyExpiration: {
							disableCustomExpiresTime: true,
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);

		const { headers, user } = await signInWithTestUser();
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey2 = await auth.api.createApiKey({
				body: {
					expiresIn: 10000,
				},
				headers,
			});
			result.data = apiKey2;
		} catch (error: any) {
			result.error = error;
		}

		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.KEY_DISABLED_EXPIRATION,
		);
	});

	it("should create an API key with an expiresIn that's smaller than the allowed minimum", async () => {
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const expiresIn = 60 * 60 * 24 * 0.5; // half a day
			const apiKey = await auth.api.createApiKey({
				body: {
					expiresIn: expiresIn,
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
		);
	});

	it("should fail to create an API key with an expiresIn that's larger than the allowed maximum", async () => {
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const expiresIn = 60 * 60 * 24 * 365 * 10; // 10 year
			const apiKey = await auth.api.createApiKey({
				body: {
					expiresIn: expiresIn,
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
		);
	});

	it("should fail to create API key with custom refillAndAmount from client auth", async () => {
		const apiKey = await client.apiKey.create(
			{
				refillAmount: 10,
			},
			{ headers },
		);

		expect(apiKey.data).toBeNull();
		expect(apiKey.error).toBeDefined();
		expect(apiKey.error?.statusText).toEqual("BAD_REQUEST");
		expect(apiKey.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);

		const apiKey2 = await client.apiKey.create(
			{
				refillInterval: 1001,
			},
			{ headers },
		);

		expect(apiKey2.data).toBeNull();
		expect(apiKey2.error).toBeDefined();
		expect(apiKey2.error?.statusText).toEqual("BAD_REQUEST");
		expect(apiKey2.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);
	});

	it("should fail to create API key when refill interval is provided, but no refill amount", async () => {
		let res: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					refillInterval: 1000,
					userId: user.id,
				},
			});
			res.data = apiKey;
		} catch (error: any) {
			res.error = error;
		}

		expect(res.data).toBeNull();
		expect(res.error).toBeDefined();
		expect(res.error?.status).toEqual("BAD_REQUEST");
		expect(res.error?.body.message).toEqual(
			ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
		);
	});

	it("should fail to create API key when refill amount is provided, but no refill interval", async () => {
		let res: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					refillAmount: 10,
					userId: user.id,
				},
			});
			res.data = apiKey;
		} catch (error: any) {
			res.error = error;
		}

		expect(res.data).toBeNull();
		expect(res.error).toBeDefined();
		expect(res.error?.status).toEqual("BAD_REQUEST");
		expect(res.error?.body.message).toEqual(
			ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
		);
	});

	it("should create the API key with the given refill interval & refill amount", async () => {
		const refillInterval = 10000;
		const refillAmount = 10;
		const apiKey = await auth.api.createApiKey({
			body: {
				refillInterval: refillInterval,
				refillAmount: refillAmount,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.refillInterval).toEqual(refillInterval);
		expect(apiKey.refillAmount).toEqual(refillAmount);
	});

	it("should create API Key with custom remaining", async () => {
		const remaining = 10;
		const apiKey = await auth.api.createApiKey({
			body: {
				remaining: remaining,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.remaining).toEqual(remaining);
	});

	it("should create API Key with remaining explicitly set to null", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				remaining: null,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.remaining).toBeNull();
	});

	it("should create API Key with remaining explicitly set to null and refillAmount and refillInterval are also set", async () => {
		const refillAmount = 10; // Arbitrary non-null value
		const refillInterval = 1000;
		const apiKey = await auth.api.createApiKey({
			body: {
				remaining: null,
				refillAmount: refillAmount,
				refillInterval: refillInterval,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.remaining).toBeNull();
		expect(apiKey.refillAmount).toBe(refillAmount);
		expect(apiKey.refillInterval).toBe(refillInterval);
	});

	it("should create API Key with remaining explicitly set to 0 and refillAmount also set", async () => {
		const remaining = 0;
		const refillAmount = 10; // Arbitrary non-null value
		const refillInterval = 1000;
		const apiKey = await auth.api.createApiKey({
			body: {
				remaining: remaining,
				refillAmount: refillAmount,
				refillInterval: refillInterval,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.remaining).toBe(remaining);
		expect(apiKey.refillAmount).toBe(refillAmount);
		expect(apiKey.refillInterval).toBe(refillInterval);
	});

	it("should create API Key with remaining undefined and default value of null is respected with refillAmount and refillInterval provided", async () => {
		const refillAmount = 10; // Arbitrary non-null value
		const refillInterval = 1000;
		const apiKey = await auth.api.createApiKey({
			body: {
				refillAmount: refillAmount,
				refillInterval: refillInterval,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.remaining).toBeNull();
		expect(apiKey.refillAmount).toBe(refillAmount);
		expect(apiKey.refillInterval).toBe(refillInterval);
	});

	it("should create API key with invalid metadata", async () => {
		let result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					metadata: "invalid",
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.INVALID_METADATA_TYPE,
		);
	});

	it("should create API key with valid metadata", async () => {
		const metadata = {
			test: "test",
		};
		const apiKey = await auth.api.createApiKey({
			body: {
				metadata: metadata,
			},
			headers,
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.metadata).toEqual(metadata);

		const res = await auth.api.getApiKey({
			query: {
				id: apiKey.id,
			},
			headers,
		});

		expect(res).not.toBeNull();
		if (res) {
			expect(res.metadata).toEqual(metadata);
		}
	});

	it("create API key's returned metadata should be an object", async () => {
		const metadata = {
			test: "test-123",
		};
		const apiKey = await auth.api.createApiKey({
			body: {
				metadata: metadata,
			},
			headers,
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.metadata.test).toBeDefined();
		expect(apiKey.metadata.test).toEqual(metadata.test);
	});

	it("create API key with with metadata when metadata is disabled (should fail)", async () => {
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						enableMetadata: false,
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const metadata = {
			test: "test-123",
		};
		const result: { data: ApiKey | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.createApiKey({
				body: {
					metadata: metadata,
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}

		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(ERROR_CODES.METADATA_DISABLED);
	});

	it("should have the first 6 chracaters of the key as the start property", async () => {
		const { data: apiKey } = await client.apiKey.create(
			{},
			{ headers: headers },
		);

		expect(apiKey?.start).toBeDefined();
		expect(apiKey?.start?.length).toEqual(6);
		expect(apiKey?.start).toEqual(apiKey?.key?.substring(0, 6));
	});

	it("should have the start property as null if shouldStore is false", async () => {
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						startingCharactersConfig: {
							shouldStore: false,
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const { data: apiKey2 } = await client.apiKey.create(
			{},
			{ headers: headers },
		);

		expect(apiKey2?.start).toBeNull();
	});

	it("should use the defined charactersLength if provided", async () => {
		const customLength = 3;
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						startingCharactersConfig: {
							shouldStore: true,
							charactersLength: customLength,
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const { data: apiKey2 } = await client.apiKey.create(
			{},
			{ headers: headers },
		);

		expect(apiKey2?.start).toBeDefined();
		expect(apiKey2?.start?.length).toEqual(customLength);
		expect(apiKey2?.start).toEqual(apiKey2?.key?.substring(0, customLength));
	});

	it("should fail to create API key with custom rate-limit options from client auth", async () => {
		const apiKey = await client.apiKey.create(
			{
				rateLimitMax: 15,
			},
			{ headers },
		);

		expect(apiKey.data).toBeNull();
		expect(apiKey.error).toBeDefined();
		expect(apiKey.error?.statusText).toEqual("BAD_REQUEST");
		expect(apiKey.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);

		const apiKey2 = await client.apiKey.create(
			{
				rateLimitTimeWindow: 1001,
			},
			{ headers },
		);

		expect(apiKey2.data).toBeNull();
		expect(apiKey2.error).toBeDefined();
		expect(apiKey2.error?.statusText).toEqual("BAD_REQUEST");
		expect(apiKey2.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY);
	});

	it("should successfully apply custom rate-limit options on the newly created API key", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				rateLimitMax: 15,
				rateLimitTimeWindow: 1000,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey?.rateLimitMax).toEqual(15);
		expect(apiKey?.rateLimitTimeWindow).toEqual(1000);
	});

	// =========================================================================
	// VERIFY API KEY
	// =========================================================================

	it("verify API key without key and userId", async () => {
		const apiKey = await auth.api.verifyApiKey({
			body: {
				key: firstApiKey.key,
			},
		});
		expect(apiKey.key).not.toBe(null);
		expect(apiKey.valid).toBe(true);
	});

	it("verify API key with invalid key (should fail)", async () => {
		const apiKey = await auth.api.verifyApiKey({
			body: {
				key: "invalid",
			},
		});
		expect(apiKey.valid).toBe(false);
		expect(apiKey.error?.code).toBe("KEY_NOT_FOUND");
	});

	let rateLimitedApiKey: ApiKey;

	const {
		client: rateLimitClient,
		auth: rateLimitAuth,
		signInWithTestUser: rateLimitTestUser,
	} = await getTestInstance(
		{
			plugins: [
				apiKey({
					rateLimit: {
						enabled: true,
						timeWindow: 1000,
					},
				}),
			],
		},
		{
			clientOptions: {
				plugins: [apiKeyClient()],
			},
		},
	);

	const { headers: rateLimitUserHeaders } = await rateLimitTestUser();

	it("should fail to verify API key 20 times in a row due to rate-limit", async () => {
		const { data: apiKey2 } = await rateLimitClient.apiKey.create(
			{},
			{ headers: rateLimitUserHeaders },
		);
		if (!apiKey2) return;
		rateLimitedApiKey = apiKey2;
		for (let i = 0; i < 20; i++) {
			const response = await rateLimitAuth.api.verifyApiKey({
				body: {
					key: apiKey2.key,
				},
				headers: rateLimitUserHeaders,
			});
			if (i >= 10) {
				expect(response.error?.code).toBe("RATE_LIMITED");
			} else {
				expect(response.error).toBeNull();
			}
		}
	});

	it("should allow us to verify API key after rate-limit window has passed", async () => {
		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(1000);
		const response = await rateLimitAuth.api.verifyApiKey({
			body: {
				key: rateLimitedApiKey.key,
			},
			headers: rateLimitUserHeaders,
		});
		expect(response.error).toBeNull();
		expect(response?.valid).toBe(true);
	});

	it("should check if verifying an API key's remaining count does go down", async () => {
		const remaining = 10;
		const { data: apiKey } = await client.apiKey.create(
			{
				remaining: remaining,
			},
			{ headers: headers },
		);
		if (!apiKey) return;
		const afterVerificationOnce = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
			headers,
		});
		expect(afterVerificationOnce?.valid).toEqual(true);
		expect(afterVerificationOnce?.key?.remaining).toEqual(remaining - 1);
		const afterVerificationTwice = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
			headers,
		});
		expect(afterVerificationTwice?.valid).toEqual(true);
		expect(afterVerificationTwice?.key?.remaining).toEqual(remaining - 2);
	});

	it("should fail if the API key has no remaining", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				remaining: 1,
				userId: user.id,
			},
		});
		if (!apiKey) return;
		// run verify once to make the remaining count go down to 0
		await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
			headers,
		});
		const afterVerification = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
			headers,
		});
		expect(afterVerification.error?.code).toBe("USAGE_EXCEEDED");
	});

	it("should fail if the API key is expired", async () => {
		vi.useRealTimers();
		const { headers } = await signInWithTestUser();
		const apiKey2 = await client.apiKey.create(
			{
				expiresIn: 60 * 60 * 24,
			},
			{ headers: headers, throw: true },
		);
		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(1000 * 60 * 60 * 24 * 2);
		const afterVerification = await auth.api.verifyApiKey({
			body: {
				key: apiKey2.key,
			},
			headers,
		});
		expect(afterVerification.error?.code).toEqual("KEY_EXPIRED");
		vi.useRealTimers();
	});

	// =========================================================================
	// UPDATE API KEY
	// =========================================================================

	it("should fail to update API key name without headers or userId", async () => {
		let error: APIError | null = null;
		await auth.api
			.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					name: "test-api-key",
				},
			})
			.catch((e) => {
				error = e;
			});
		expect(error).not.toBeNull();
		expect(error).toBeInstanceOf(APIError);
	});

	it("should update API key name with headers", async () => {
		const newName = "Hello World";
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: firstApiKey.id,
				name: newName,
			},
			headers,
		});
		expect(apiKey).toBeDefined();
		expect(apiKey.name).not.toEqual(firstApiKey.name);
		expect(apiKey.name).toEqual(newName);
	});

	it("should fail to update API key name with a length larger than the allowed maximum", async () => {
		let error: APIError | null = null;
		await auth.api
			.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					name: "test-api-key-that-is-longer-than-the-allowed-maximum",
				},
				headers,
			})
			.catch((e) => {
				if (e instanceof APIError) {
					error = e;
					expect(error?.status).toEqual("BAD_REQUEST");
					expect(error?.body?.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
				}
			});
		expect(error).not.toBeNull();
	});

	it("should fail to update API key name with a length smaller than the allowed minimum", async () => {
		let error: APIError | null = null;
		await auth.api
			.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					name: "",
				},
				headers,
			})
			.catch((e) => {
				if (e instanceof APIError) {
					error = e;
					expect(error?.status).toEqual("BAD_REQUEST");
					expect(error?.body?.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH);
				}
			});
		expect(error).not.toBeNull();
	});

	it("should fail to update API key with no values to update", async () => {
		let error: APIError | null = null;
		await auth.api
			.updateApiKey({
				body: {
					keyId: firstApiKey.id,
				},
				headers,
			})
			.catch((e) => {
				if (e instanceof APIError) {
					error = e;
					expect(error?.status).toEqual("BAD_REQUEST");
					expect(error?.body?.message).toEqual(ERROR_CODES.NO_VALUES_TO_UPDATE);
				}
			});
		expect(error).not.toBeNull();
	});

	it("should update API key expiresIn value", async () => {
		const expiresIn = 60 * 60 * 24 * 7; // 7 days
		const expectedResult = new Date().getTime() + expiresIn;
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: firstApiKey.id,
				expiresIn: expiresIn,
			},
			headers,
		});
		expect(apiKey).not.toBeNull();
		expect(apiKey.expiresAt).toBeDefined();
		expect(apiKey.expiresAt?.getTime()).toBeGreaterThanOrEqual(expectedResult);
	});

	it("should fail to update expiresIn value if `disableCustomExpiresTime` is enabled", async () => {
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						keyExpiration: {
							disableCustomExpiresTime: true,
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const { data: firstApiKey } = await client.apiKey.create({}, { headers });

		if (!firstApiKey) return;

		let result: { data: Partial<ApiKey> | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					expiresIn: 1000 * 60 * 60 * 24 * 7, // 7 days
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.KEY_DISABLED_EXPIRATION,
		);
	});

	it("should fail to update expiresIn value if it's smaller than the allowed minimum", async () => {
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						keyExpiration: {
							minExpiresIn: 1,
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const { data: firstApiKey } = await client.apiKey.create({}, { headers });

		if (!firstApiKey) return;

		let result: { data: Partial<ApiKey> | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					expiresIn: 1,
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
		);
	});

	it("should fail to update expiresIn value if it's larger than the allowed maximum", async () => {
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						keyExpiration: {
							maxExpiresIn: 1,
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);
		const { headers } = await signInWithTestUser();

		const { data: firstApiKey } = await client.apiKey.create({}, { headers });

		if (!firstApiKey) return;

		let result: { data: Partial<ApiKey> | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					expiresIn: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
		);
	});

	it("should update API key remaining count", async () => {
		const remaining = 100;
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: firstApiKey.id,
				remaining: remaining,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.remaining).toEqual(remaining);
	});

	it("should fail update the refillInterval value since it requires refillAmount as well", async () => {
		let result: { data: Partial<ApiKey> | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					refillInterval: 1000,
					userId: user.id,
				},
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
		);
	});

	it("should fail update the refillAmount value since it requires refillInterval as well", async () => {
		let result: { data: Partial<ApiKey> | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					refillAmount: 10,
					userId: user.id,
				},
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
		);
	});

	it("should update the refillInterval and refillAmount value", async () => {
		const refillInterval = 10000;
		const refillAmount = 100;
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: firstApiKey.id,
				refillInterval: refillInterval,
				refillAmount: refillAmount,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.refillInterval).toEqual(refillInterval);
		expect(apiKey.refillAmount).toEqual(refillAmount);
	});

	it("should update API key enable value", async () => {
		const newValue = false;
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: firstApiKey.id,
				enabled: newValue,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.enabled).toEqual(newValue);
	});

	it("should fail to update metadata with invalid metadata type", async () => {
		let result: { data: Partial<ApiKey> | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.updateApiKey({
				body: {
					keyId: firstApiKey.id,
					metadata: "invalid",
					userId: user.id,
				},
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("BAD_REQUEST");
		expect(result.error?.body.message).toEqual(
			ERROR_CODES.INVALID_METADATA_TYPE,
		);
	});

	it("should update metadata with valid metadata type", async () => {
		const metadata = {
			test: "test-123",
		};
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: firstApiKey.id,
				metadata: metadata,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.metadata).toEqual(metadata);
	});

	it("update API key's returned metadata should be an object", async () => {
		const metadata = {
			test: "test-12345",
		};
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: firstApiKey.id,
				metadata: metadata,
				userId: user.id,
			},
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.metadata?.test).toBeDefined();
		expect(apiKey.metadata?.test).toEqual(metadata.test);
	});

	// =========================================================================
	// GET API KEY
	// =========================================================================

	it("should get an API key by id", async () => {
		const apiKey = await client.apiKey.get({
			query: {
				id: firstApiKey.id,
			},
			fetchOptions: {
				headers,
			},
		});
		expect(apiKey.data).not.toBeNull();
		expect(apiKey.data?.id).toBe(firstApiKey.id);
	});

	it("should fail to get an API key by ID that doesn't exist", async () => {
		const result = await client.apiKey.get(
			{
				query: {
					id: "invalid",
				},
			},
			{ headers },
		);
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual(404);
	});

	it("should successfully receive an object metadata from an API key", async () => {
		const apiKey = await client.apiKey.get(
			{
				query: {
					id: firstApiKey.id,
				},
			},
			{
				headers,
			},
		);
		expect(apiKey).not.toBeNull();
		expect(apiKey.data?.metadata).toBeDefined();
		expect(apiKey.data?.metadata).toBeInstanceOf(Object);
	});

	// =========================================================================
	// LIST API KEY
	// =========================================================================

	it("should fail to list API keys without headers", async () => {
		let result: { data: Partial<ApiKey>[] | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.listApiKeys({});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}

		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("UNAUTHORIZED");
	});

	it("should list API keys with headers", async () => {
		const apiKeys = await auth.api.listApiKeys({
			headers,
		});

		expect(apiKeys).not.toBeNull();
		expect(apiKeys.length).toBeGreaterThan(0);
	});

	it("should list API keys with metadata as an object", async () => {
		const apiKeys = await auth.api.listApiKeys({
			headers,
		});

		expect(apiKeys).not.toBeNull();
		expect(apiKeys.length).toBeGreaterThan(0);
		apiKeys.map((apiKey) => {
			if (apiKey.metadata) {
				expect(apiKey.metadata).toBeInstanceOf(Object);
			}
		});
	});

	// =========================================================================
	// Sessions from API keys
	// =========================================================================

	it("should get session from an API key", async () => {
		const { client, auth, signInWithTestUser } = await getTestInstance(
			{
				plugins: [
					apiKey({
						enableSessionForAPIKeys: true,
					}),
				],
			},
			{
				clientOptions: {
					plugins: [apiKeyClient()],
				},
			},
		);

		const { headers: userHeaders } = await signInWithTestUser();

		const { data: apiKey2 } = await client.apiKey.create(
			{},
			{ headers: userHeaders },
		);
		if (!apiKey2) return;
		const headers = new Headers();
		headers.set("x-api-key", apiKey2.key);

		const session = await auth.api.getSession({
			headers: headers,
		});

		expect(session?.session).toBeDefined();
	});

	// =========================================================================
	// DELETE API KEY
	// =========================================================================

	it("should fail to delete an API key by ID without headers", async () => {
		let result: { data: { success: boolean } | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.deleteApiKey({
				body: {
					keyId: firstApiKey.id,
				},
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}

		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("UNAUTHORIZED");
	});

	it("should delete an API key by ID with headers", async () => {
		const apiKey = await auth.api.deleteApiKey({
			body: {
				keyId: firstApiKey.id,
			},
			headers,
		});

		expect(apiKey).not.toBeNull();
		expect(apiKey.success).toEqual(true);
	});

	it("should delete an API key by ID with headers using auth-client", async () => {
		const newApiKey = await client.apiKey.create({}, { headers: headers });
		if (!newApiKey.data) return;

		const apiKey = await client.apiKey.delete(
			{
				keyId: newApiKey.data.id,
			},
			{ headers },
		);

		if (!apiKey.data?.success) {
			console.log(apiKey.error);
		}

		expect(apiKey).not.toBeNull();
		expect(apiKey.data?.success).toEqual(true);
	});

	it("should fail to delete an API key by ID that doesn't exist", async () => {
		let result: { data: { success: boolean } | null; error: Err | null } = {
			data: null,
			error: null,
		};
		try {
			const apiKey = await auth.api.deleteApiKey({
				body: {
					keyId: "invalid",
				},
				headers,
			});
			result.data = apiKey;
		} catch (error: any) {
			result.error = error;
		}
		expect(result.data).toBeNull();
		expect(result.error).toBeDefined();
		expect(result.error?.status).toEqual("NOT_FOUND");
		expect(result.error?.body.message).toEqual(ERROR_CODES.KEY_NOT_FOUND);
	});

	it("should create an API key with permissions", async () => {
		const permissions = {
			files: ["read", "write"],
			users: ["read"],
		};

		const apiKey = await auth.api.createApiKey({
			body: {
				permissions,
				userId: user.id,
			},
		});
		expect(apiKey).not.toBeNull();
		expect(apiKey.permissions).toEqual(permissions);
	});

	it("should have permissions as an object from getApiKey", async () => {
		const permissions = {
			files: ["read", "write"],
			users: ["read"],
		};

		const apiKey = await auth.api.createApiKey({
			body: {
				permissions,
				userId: user.id,
			},
		});

		const apiKeyResults = await auth.api.getApiKey({
			query: {
				id: apiKey.id,
			},
			headers,
		});

		expect(apiKeyResults).not.toBeNull();
		expect(apiKeyResults.permissions).toEqual(permissions);
	});

	it("should have permissions as an object from verifyApiKey", async () => {
		const permissions = {
			files: ["read", "write"],
			users: ["read"],
		};

		const apiKey = await auth.api.createApiKey({
			body: {
				permissions,
				userId: user.id,
			},
		});
		const apiKeyResults = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
				permissions: {
					files: ["read"],
				},
			},
			headers,
		});

		expect(apiKeyResults).not.toBeNull();
		expect(apiKeyResults.key?.permissions).toEqual(permissions);
	});

	it("should create an API key with default permissions", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
			},
		});
		expect(apiKey).not.toBeNull();
		expect(apiKey.permissions).toEqual({
			files: ["read"],
		});
	});

	it("should have valid metadata from key verification results", async () => {
		const metadata = {
			test: "hello-world-123",
		};
		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
				metadata: metadata,
			},
			headers,
		});

		expect(apiKey).not.toBeNull();
		if (apiKey) {
			const result = await auth.api.verifyApiKey({
				body: {
					key: apiKey.key,
				},
				headers,
			});

			expect(result.valid).toBe(true);
			expect(result.error).toBeNull();
			expect(result.key?.metadata).toEqual(metadata);
		}
	});

	it("should verify an API key with matching permissions", async () => {
		const permissions = {
			files: ["read", "write"],
			users: ["read"],
		};

		const apiKey = await auth.api.createApiKey({
			body: {
				permissions,
				userId: user.id,
			},
		});

		const result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
				permissions: {
					files: ["read"],
				},
			},
		});

		expect(result.valid).toBe(true);
		expect(result.error).toBeNull();
		expect(result.key?.permissions).toEqual(permissions);
	});

	it("should fail to verify an API key with non-matching permissions", async () => {
		const permissions = {
			files: ["read"],
			users: ["read"],
		};

		const apiKey = await auth.api.createApiKey({
			body: {
				permissions,
				userId: user.id,
			},
		});

		const result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
				permissions: {
					files: ["write"],
				},
			},
		});

		expect(result.valid).toBe(false);
		expect(result.error?.code).toBe("KEY_NOT_FOUND");
	});

	it("should fail to verify when required permissions are specified but API key has no permissions", async () => {
		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
			},
		});

		const result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
				permissions: {
					files: ["write"],
				},
			},
		});

		expect(result.valid).toBe(false);
		expect(result.error?.code).toBe("KEY_NOT_FOUND");
	});

	it("should update an API key with permissions", async () => {
		const permissions = {
			files: ["read", "write"],
			users: ["read"],
		};
		const createdApiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
			},
		});
		expect(createdApiKey.permissions).not.toEqual(permissions);
		const apiKey = await auth.api.updateApiKey({
			body: {
				keyId: createdApiKey.id,
				permissions,
				userId: user.id,
			},
		});
		expect(apiKey).not.toBeNull();
		expect(apiKey.permissions).toEqual(permissions);
	});

	it("should refill API key credits after refill interval (milliseconds)", async () => {
		vi.useRealTimers();

		const refillInterval = 3600000; // 1 hour in milliseconds
		const refillAmount = 5;
		const initialRemaining = 2;

		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
				remaining: initialRemaining,
				refillInterval: refillInterval,
				refillAmount: refillAmount,
			},
		});

		let result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(initialRemaining - 1);

		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(0);

		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(false);
		expect(result.error?.code).toBe("USAGE_EXCEEDED");

		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(refillInterval + 1000);

		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(refillAmount - 1);

		vi.useRealTimers();
	});

	it("should not refill API key credits before refill interval expires", async () => {
		vi.useRealTimers();

		const refillInterval = 86400000; // 24 hours in milliseconds
		const refillAmount = 10;
		const initialRemaining = 1;

		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
				remaining: initialRemaining,
				refillInterval: refillInterval,
				refillAmount: refillAmount,
			},
		});

		let result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(0);

		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(refillInterval / 2); // Only advance half the interval

		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(false);
		expect(result.error?.code).toBe("USAGE_EXCEEDED");

		await vi.advanceTimersByTimeAsync(refillInterval / 2 + 1000);

		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(refillAmount - 1);

		vi.useRealTimers();
	});

	it("should handle multiple refill cycles correctly", async () => {
		vi.useRealTimers();

		const refillInterval = 3600000; // 1 hour in milliseconds
		const refillAmount = 3;

		const apiKey = await auth.api.createApiKey({
			body: {
				userId: user.id,
				remaining: 1,
				refillInterval: refillInterval,
				refillAmount: refillAmount,
			},
		});

		let result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(0);

		vi.useFakeTimers();

		await vi.advanceTimersByTimeAsync(refillInterval + 1000);
		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(refillAmount - 1);

		for (let i = 0; i < refillAmount - 1; i++) {
			result = await auth.api.verifyApiKey({
				body: {
					key: apiKey.key,
				},
			});
			expect(result.valid).toBe(true);
		}

		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(false);
		expect(result.error?.code).toBe("USAGE_EXCEEDED");

		await vi.advanceTimersByTimeAsync(refillInterval + 1000);
		result = await auth.api.verifyApiKey({
			body: {
				key: apiKey.key,
			},
		});
		expect(result.valid).toBe(true);
		expect(result.key?.remaining).toBe(refillAmount - 1);

		vi.useRealTimers();
	});
});

```
Page 46/49FirstPrevNextLast