This is page 46 of 49. Use http://codebase.md/better-auth/better-auth?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(); }); }); ```