This is page 63 of 68. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── 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-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.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 │ │ │ │ ├── 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 │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── 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 │ │ │ ├── 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/api-key/api-key.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { apiKey, ERROR_CODES } from "."; 4 | import { apiKeyClient } from "./client"; 5 | import type { ApiKey } from "./types"; 6 | import { APIError } from "better-call"; 7 | 8 | describe("api-key", async () => { 9 | const { client, auth, signInWithTestUser } = await getTestInstance( 10 | { 11 | plugins: [ 12 | apiKey({ 13 | enableMetadata: true, 14 | permissions: { 15 | defaultPermissions: { 16 | files: ["read"], 17 | }, 18 | }, 19 | }), 20 | ], 21 | }, 22 | { 23 | clientOptions: { 24 | plugins: [apiKeyClient()], 25 | }, 26 | }, 27 | ); 28 | const { headers, user } = await signInWithTestUser(); 29 | 30 | // ========================================================================= 31 | // CREATE API KEY 32 | // ========================================================================= 33 | 34 | it("should fail to create API keys from client without headers", async () => { 35 | const apiKeyFail = await client.apiKey.create(); 36 | 37 | expect(apiKeyFail.data).toBeNull(); 38 | expect(apiKeyFail.error).toBeDefined(); 39 | expect(apiKeyFail.error?.status).toEqual(401); 40 | expect(apiKeyFail.error?.statusText).toEqual("UNAUTHORIZED"); 41 | expect(apiKeyFail.error?.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION); 42 | }); 43 | 44 | let firstApiKey: ApiKey; 45 | 46 | it("should successfully create API keys from client with headers", async () => { 47 | const apiKey = await client.apiKey.create({}, { headers: headers }); 48 | if (apiKey.data) { 49 | firstApiKey = apiKey.data; 50 | } 51 | 52 | expect(apiKey.data).not.toBeNull(); 53 | expect(apiKey.data?.key).toBeDefined(); 54 | expect(apiKey.data?.userId).toEqual(user.id); 55 | expect(apiKey.data?.name).toBeNull(); 56 | expect(apiKey.data?.prefix).toBeNull(); 57 | expect(apiKey.data?.refillInterval).toBeNull(); 58 | expect(apiKey.data?.refillAmount).toBeNull(); 59 | expect(apiKey.data?.lastRefillAt).toBeNull(); 60 | expect(apiKey.data?.enabled).toEqual(true); 61 | expect(apiKey.data?.rateLimitTimeWindow).toEqual(86400000); 62 | expect(apiKey.data?.rateLimitMax).toEqual(10); 63 | expect(apiKey.data?.requestCount).toEqual(0); 64 | expect(apiKey.data?.remaining).toBeNull(); 65 | expect(apiKey.data?.lastRequest).toBeNull(); 66 | expect(apiKey.data?.expiresAt).toBeNull(); 67 | expect(apiKey.data?.createdAt).toBeDefined(); 68 | expect(apiKey.data?.updatedAt).toBeDefined(); 69 | expect(apiKey.data?.metadata).toBeNull(); 70 | expect(apiKey.error).toBeNull(); 71 | }); 72 | 73 | interface Err { 74 | body: { 75 | code: string | undefined; 76 | message: string | undefined; 77 | }; 78 | status: string; 79 | statusCode: string; 80 | } 81 | 82 | it("should fail to create API Keys from server without headers and userId", async () => { 83 | let res: { data: ApiKey | null; error: Err | null } = { 84 | data: null, 85 | error: null, 86 | }; 87 | try { 88 | const apiKey = await auth.api.createApiKey({ body: {} }); 89 | res.data = apiKey; 90 | } catch (error: any) { 91 | res.error = error; 92 | } 93 | 94 | expect(res.data).toBeNull(); 95 | expect(res.error).toBeDefined(); 96 | expect(res.error?.statusCode).toEqual(401); 97 | expect(res.error?.status).toEqual("UNAUTHORIZED"); 98 | expect(res.error?.body.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION); 99 | }); 100 | 101 | it("should fail to create api keys from the client if user id is provided", async () => { 102 | const { headers, user } = await signInWithTestUser(); 103 | const response = await client.apiKey.create({ 104 | userId: user.id, 105 | }); 106 | expect(response.error?.status).toBe(401); 107 | const newUser = await auth.api.signUpEmail({ 108 | body: { 109 | email: "[email protected]", 110 | password: "password", 111 | name: "test-name", 112 | }, 113 | }); 114 | const response2 = await client.apiKey.create( 115 | { 116 | userId: newUser.user.id, 117 | }, 118 | { 119 | headers, 120 | }, 121 | ); 122 | expect(response2.error?.status).toBe(401); 123 | }); 124 | 125 | it("should successfully create API keys from server with userId", async () => { 126 | const apiKey = await auth.api.createApiKey({ 127 | body: { 128 | userId: user.id, 129 | }, 130 | }); 131 | 132 | expect(apiKey).not.toBeNull(); 133 | expect(apiKey.key).toBeDefined(); 134 | expect(apiKey.userId).toEqual(user.id); 135 | expect(apiKey.name).toBeNull(); 136 | expect(apiKey.prefix).toBeNull(); 137 | expect(apiKey.refillInterval).toBeNull(); 138 | expect(apiKey.refillAmount).toBeNull(); 139 | expect(apiKey.lastRefillAt).toBeNull(); 140 | expect(apiKey.enabled).toEqual(true); 141 | expect(apiKey.rateLimitTimeWindow).toEqual(86400000); 142 | expect(apiKey.rateLimitMax).toEqual(10); 143 | expect(apiKey.requestCount).toEqual(0); 144 | expect(apiKey.remaining).toBeNull(); 145 | expect(apiKey.lastRequest).toBeNull(); 146 | expect(apiKey.rateLimitEnabled).toBe(true); 147 | }); 148 | 149 | it("should have the real value from rateLimitEnabled", async () => { 150 | const apiKey = await auth.api.createApiKey({ 151 | body: { 152 | userId: user.id, 153 | rateLimitEnabled: false, 154 | }, 155 | }); 156 | 157 | expect(apiKey).not.toBeNull(); 158 | expect(apiKey.rateLimitEnabled).toBe(false); 159 | }); 160 | 161 | it("should have true if the rate limit is undefined", async () => { 162 | const apiKey = await auth.api.createApiKey({ 163 | body: { 164 | userId: user.id, 165 | rateLimitEnabled: undefined, 166 | }, 167 | }); 168 | 169 | expect(apiKey).not.toBeNull(); 170 | expect(apiKey.rateLimitEnabled).toBe(true); 171 | }); 172 | 173 | it("should require name in API keys if configured", async () => { 174 | const { auth, signInWithTestUser } = await getTestInstance( 175 | { 176 | plugins: [ 177 | apiKey({ 178 | requireName: true, 179 | }), 180 | ], 181 | }, 182 | { 183 | clientOptions: { 184 | plugins: [apiKeyClient()], 185 | }, 186 | }, 187 | ); 188 | 189 | const { user } = await signInWithTestUser(); 190 | let err: any; 191 | try { 192 | await auth.api.createApiKey({ 193 | body: { 194 | userId: user.id, 195 | }, 196 | }); 197 | } catch (error) { 198 | err = error; 199 | } 200 | expect(err).toBeDefined(); 201 | expect(err.body.message).toBe(ERROR_CODES.NAME_REQUIRED); 202 | }); 203 | 204 | it("should respect rateLimit configuration from plugin options", async () => { 205 | const { auth, signInWithTestUser } = await getTestInstance( 206 | { 207 | plugins: [ 208 | apiKey({ 209 | rateLimit: { 210 | enabled: false, 211 | timeWindow: 1000, 212 | maxRequests: 10, 213 | }, 214 | enableMetadata: true, 215 | }), 216 | ], 217 | }, 218 | { 219 | clientOptions: { 220 | plugins: [apiKeyClient()], 221 | }, 222 | }, 223 | ); 224 | 225 | const { user } = await signInWithTestUser(); 226 | const apiKeyResult = await auth.api.createApiKey({ 227 | body: { 228 | userId: user.id, 229 | }, 230 | }); 231 | 232 | expect(apiKeyResult).not.toBeNull(); 233 | expect(apiKeyResult.rateLimitEnabled).toBe(false); 234 | expect(apiKeyResult.rateLimitTimeWindow).toBe(1000); 235 | expect(apiKeyResult.rateLimitMax).toBe(10); 236 | }); 237 | 238 | it("should create the API key with the given name", async () => { 239 | const apiKey = await auth.api.createApiKey({ 240 | body: { 241 | name: "test-api-key", 242 | }, 243 | headers, 244 | }); 245 | 246 | expect(apiKey).not.toBeNull(); 247 | expect(apiKey.name).toEqual("test-api-key"); 248 | }); 249 | 250 | it("should create the API key with a name that's shorter than the allowed minimum", async () => { 251 | let result: { data: ApiKey | null; error: Err | null } = { 252 | data: null, 253 | error: null, 254 | }; 255 | try { 256 | const apiKey = await auth.api.createApiKey({ 257 | body: { 258 | name: "test-api-key-that-is-shorter-than-the-allowed-minimum", 259 | }, 260 | headers, 261 | }); 262 | result.data = apiKey; 263 | } catch (error: any) { 264 | result.error = error; 265 | } 266 | expect(result.data).toBeNull(); 267 | expect(result.error).toBeDefined(); 268 | expect(result.error?.status).toEqual("BAD_REQUEST"); 269 | expect(result.error?.body.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH); 270 | }); 271 | 272 | it("should create the API key with a name that's longer than the allowed maximum", async () => { 273 | let result: { data: ApiKey | null; error: Err | null } = { 274 | data: null, 275 | error: null, 276 | }; 277 | try { 278 | const apiKey = await auth.api.createApiKey({ 279 | body: { 280 | name: "test-api-key-that-is-longer-than-the-allowed-maximum", 281 | }, 282 | headers, 283 | }); 284 | result.data = apiKey; 285 | } catch (error: any) { 286 | result.error = error; 287 | } 288 | expect(result.data).toBeNull(); 289 | expect(result.error).toBeDefined(); 290 | expect(result.error?.status).toEqual("BAD_REQUEST"); 291 | expect(result.error?.body.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH); 292 | }); 293 | 294 | it("should create the API key with the given prefix", async () => { 295 | const prefix = "test-api-key_"; 296 | const apiKey = await auth.api.createApiKey({ 297 | body: { 298 | prefix: prefix, 299 | }, 300 | headers, 301 | }); 302 | 303 | expect(apiKey).not.toBeNull(); 304 | expect(apiKey.prefix).toEqual(prefix); 305 | expect(apiKey.key.startsWith(prefix)).toEqual(true); 306 | }); 307 | 308 | it("should create the API key with a prefix that's shorter than the allowed minimum", async () => { 309 | let result: { data: ApiKey | null; error: Err | null } = { 310 | data: null, 311 | error: null, 312 | }; 313 | try { 314 | const apiKey = await auth.api.createApiKey({ 315 | body: { 316 | prefix: "test-api-key-that-is-shorter-than-the-allowed-minimum", 317 | }, 318 | headers, 319 | }); 320 | result.data = apiKey; 321 | } catch (error: any) { 322 | result.error = error; 323 | } 324 | expect(result.data).toBeNull(); 325 | expect(result.error).toBeDefined(); 326 | expect(result.error?.status).toEqual("BAD_REQUEST"); 327 | expect(result.error?.body.message).toEqual( 328 | ERROR_CODES.INVALID_PREFIX_LENGTH, 329 | ); 330 | }); 331 | 332 | it("should create the API key with a prefix that's longer than the allowed maximum", async () => { 333 | let result: { data: ApiKey | null; error: Err | null } = { 334 | data: null, 335 | error: null, 336 | }; 337 | try { 338 | const apiKey = await auth.api.createApiKey({ 339 | body: { 340 | prefix: "test-api-key-that-is-longer-than-the-allowed-maximum", 341 | }, 342 | headers, 343 | }); 344 | result.data = apiKey; 345 | } catch (error: any) { 346 | result.error = error; 347 | } 348 | expect(result.data).toBeNull(); 349 | expect(result.error).toBeDefined(); 350 | expect(result.error?.status).toEqual("BAD_REQUEST"); 351 | expect(result.error?.body.message).toEqual( 352 | ERROR_CODES.INVALID_PREFIX_LENGTH, 353 | ); 354 | }); 355 | 356 | it("should create an API key with a custom expiresIn", async () => { 357 | const expiresIn = 60 * 60 * 24 * 7; // 7 days 358 | const expectedResult = new Date().getTime() + expiresIn; 359 | const apiKey = await auth.api.createApiKey({ 360 | body: { 361 | expiresIn: expiresIn, 362 | }, 363 | headers, 364 | }); 365 | expect(apiKey).not.toBeNull(); 366 | expect(apiKey.expiresAt).toBeDefined(); 367 | expect(apiKey.expiresAt?.getTime()).toBeGreaterThanOrEqual(expectedResult); 368 | }); 369 | 370 | it("should support disabling key hashing", async () => { 371 | const { auth, signInWithTestUser } = await getTestInstance( 372 | { 373 | plugins: [ 374 | apiKey({ 375 | disableKeyHashing: true, 376 | }), 377 | ], 378 | }, 379 | { 380 | clientOptions: { 381 | plugins: [apiKeyClient()], 382 | }, 383 | }, 384 | ); 385 | const { headers } = await signInWithTestUser(); 386 | 387 | const apiKey2 = await auth.api.createApiKey({ 388 | body: {}, 389 | headers, 390 | }); 391 | const res = await (await auth.$context).adapter.findOne<ApiKey>({ 392 | model: "apikey", 393 | where: [ 394 | { 395 | field: "id", 396 | value: apiKey2.id, 397 | }, 398 | ], 399 | }); 400 | expect(res?.key).toEqual(apiKey2.key); 401 | }); 402 | 403 | it("should be able to verify with key hashing disabled", async () => { 404 | const { auth, signInWithTestUser } = await getTestInstance( 405 | { 406 | plugins: [ 407 | apiKey({ 408 | disableKeyHashing: true, 409 | }), 410 | ], 411 | }, 412 | { 413 | clientOptions: { 414 | plugins: [apiKeyClient()], 415 | }, 416 | }, 417 | ); 418 | const { headers } = await signInWithTestUser(); 419 | 420 | const apiKey2 = await auth.api.createApiKey({ 421 | body: {}, 422 | headers, 423 | }); 424 | 425 | const result = await auth.api.verifyApiKey({ body: { key: apiKey2.key } }); 426 | expect(result.valid).toEqual(true); 427 | }); 428 | 429 | it("should fail to create a key with a custom expiresIn value when customExpiresTime is disabled", async () => { 430 | const { client, auth, signInWithTestUser } = await getTestInstance( 431 | { 432 | plugins: [ 433 | apiKey({ 434 | enableMetadata: true, 435 | keyExpiration: { 436 | disableCustomExpiresTime: true, 437 | }, 438 | }), 439 | ], 440 | }, 441 | { 442 | clientOptions: { 443 | plugins: [apiKeyClient()], 444 | }, 445 | }, 446 | ); 447 | 448 | const { headers, user } = await signInWithTestUser(); 449 | let result: { data: ApiKey | null; error: Err | null } = { 450 | data: null, 451 | error: null, 452 | }; 453 | try { 454 | const apiKey2 = await auth.api.createApiKey({ 455 | body: { 456 | expiresIn: 10000, 457 | }, 458 | headers, 459 | }); 460 | result.data = apiKey2; 461 | } catch (error: any) { 462 | result.error = error; 463 | } 464 | 465 | expect(result.data).toBeNull(); 466 | expect(result.error).toBeDefined(); 467 | expect(result.error?.body.message).toEqual( 468 | ERROR_CODES.KEY_DISABLED_EXPIRATION, 469 | ); 470 | }); 471 | 472 | it("should create an API key with an expiresIn that's smaller than the allowed minimum", async () => { 473 | let result: { data: ApiKey | null; error: Err | null } = { 474 | data: null, 475 | error: null, 476 | }; 477 | try { 478 | const expiresIn = 60 * 60 * 24 * 0.5; // half a day 479 | const apiKey = await auth.api.createApiKey({ 480 | body: { 481 | expiresIn: expiresIn, 482 | }, 483 | headers, 484 | }); 485 | result.data = apiKey; 486 | } catch (error: any) { 487 | result.error = error; 488 | } 489 | expect(result.data).toBeNull(); 490 | expect(result.error).toBeDefined(); 491 | expect(result.error?.status).toEqual("BAD_REQUEST"); 492 | expect(result.error?.body.message).toEqual( 493 | ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL, 494 | ); 495 | }); 496 | 497 | it("should fail to create an API key with an expiresIn that's larger than the allowed maximum", async () => { 498 | let result: { data: ApiKey | null; error: Err | null } = { 499 | data: null, 500 | error: null, 501 | }; 502 | try { 503 | const expiresIn = 60 * 60 * 24 * 365 * 10; // 10 year 504 | const apiKey = await auth.api.createApiKey({ 505 | body: { 506 | expiresIn: expiresIn, 507 | }, 508 | headers, 509 | }); 510 | result.data = apiKey; 511 | } catch (error: any) { 512 | result.error = error; 513 | } 514 | expect(result.data).toBeNull(); 515 | expect(result.error).toBeDefined(); 516 | expect(result.error?.status).toEqual("BAD_REQUEST"); 517 | expect(result.error?.body.message).toEqual( 518 | ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE, 519 | ); 520 | }); 521 | 522 | it("should fail to create API key with custom refillAndAmount from client auth", async () => { 523 | const apiKey = await client.apiKey.create( 524 | { 525 | refillAmount: 10, 526 | }, 527 | { headers }, 528 | ); 529 | 530 | expect(apiKey.data).toBeNull(); 531 | expect(apiKey.error).toBeDefined(); 532 | expect(apiKey.error?.statusText).toEqual("BAD_REQUEST"); 533 | expect(apiKey.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY); 534 | 535 | const apiKey2 = await client.apiKey.create( 536 | { 537 | refillInterval: 1001, 538 | }, 539 | { headers }, 540 | ); 541 | 542 | expect(apiKey2.data).toBeNull(); 543 | expect(apiKey2.error).toBeDefined(); 544 | expect(apiKey2.error?.statusText).toEqual("BAD_REQUEST"); 545 | expect(apiKey2.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY); 546 | }); 547 | 548 | it("should fail to create API key when refill interval is provided, but no refill amount", async () => { 549 | let res: { data: ApiKey | null; error: Err | null } = { 550 | data: null, 551 | error: null, 552 | }; 553 | try { 554 | const apiKey = await auth.api.createApiKey({ 555 | body: { 556 | refillInterval: 1000, 557 | userId: user.id, 558 | }, 559 | }); 560 | res.data = apiKey; 561 | } catch (error: any) { 562 | res.error = error; 563 | } 564 | 565 | expect(res.data).toBeNull(); 566 | expect(res.error).toBeDefined(); 567 | expect(res.error?.status).toEqual("BAD_REQUEST"); 568 | expect(res.error?.body.message).toEqual( 569 | ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED, 570 | ); 571 | }); 572 | 573 | it("should fail to create API key when refill amount is provided, but no refill interval", async () => { 574 | let res: { data: ApiKey | null; error: Err | null } = { 575 | data: null, 576 | error: null, 577 | }; 578 | try { 579 | const apiKey = await auth.api.createApiKey({ 580 | body: { 581 | refillAmount: 10, 582 | userId: user.id, 583 | }, 584 | }); 585 | res.data = apiKey; 586 | } catch (error: any) { 587 | res.error = error; 588 | } 589 | 590 | expect(res.data).toBeNull(); 591 | expect(res.error).toBeDefined(); 592 | expect(res.error?.status).toEqual("BAD_REQUEST"); 593 | expect(res.error?.body.message).toEqual( 594 | ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED, 595 | ); 596 | }); 597 | 598 | it("should create the API key with the given refill interval & refill amount", async () => { 599 | const refillInterval = 10000; 600 | const refillAmount = 10; 601 | const apiKey = await auth.api.createApiKey({ 602 | body: { 603 | refillInterval: refillInterval, 604 | refillAmount: refillAmount, 605 | userId: user.id, 606 | }, 607 | }); 608 | 609 | expect(apiKey).not.toBeNull(); 610 | expect(apiKey.refillInterval).toEqual(refillInterval); 611 | expect(apiKey.refillAmount).toEqual(refillAmount); 612 | }); 613 | 614 | it("should create API Key with custom remaining", async () => { 615 | const remaining = 10; 616 | const apiKey = await auth.api.createApiKey({ 617 | body: { 618 | remaining: remaining, 619 | userId: user.id, 620 | }, 621 | }); 622 | 623 | expect(apiKey).not.toBeNull(); 624 | expect(apiKey.remaining).toEqual(remaining); 625 | }); 626 | 627 | it("should create API Key with remaining explicitly set to null", async () => { 628 | const apiKey = await auth.api.createApiKey({ 629 | body: { 630 | remaining: null, 631 | userId: user.id, 632 | }, 633 | }); 634 | 635 | expect(apiKey).not.toBeNull(); 636 | expect(apiKey.remaining).toBeNull(); 637 | }); 638 | 639 | it("should create API Key with remaining explicitly set to null and refillAmount and refillInterval are also set", async () => { 640 | const refillAmount = 10; // Arbitrary non-null value 641 | const refillInterval = 1000; 642 | const apiKey = await auth.api.createApiKey({ 643 | body: { 644 | remaining: null, 645 | refillAmount: refillAmount, 646 | refillInterval: refillInterval, 647 | userId: user.id, 648 | }, 649 | }); 650 | 651 | expect(apiKey).not.toBeNull(); 652 | expect(apiKey.remaining).toBeNull(); 653 | expect(apiKey.refillAmount).toBe(refillAmount); 654 | expect(apiKey.refillInterval).toBe(refillInterval); 655 | }); 656 | 657 | it("should create API Key with remaining explicitly set to 0 and refillAmount also set", async () => { 658 | const remaining = 0; 659 | const refillAmount = 10; // Arbitrary non-null value 660 | const refillInterval = 1000; 661 | const apiKey = await auth.api.createApiKey({ 662 | body: { 663 | remaining: remaining, 664 | refillAmount: refillAmount, 665 | refillInterval: refillInterval, 666 | userId: user.id, 667 | }, 668 | }); 669 | 670 | expect(apiKey).not.toBeNull(); 671 | expect(apiKey.remaining).toBe(remaining); 672 | expect(apiKey.refillAmount).toBe(refillAmount); 673 | expect(apiKey.refillInterval).toBe(refillInterval); 674 | }); 675 | 676 | it("should create API Key with remaining undefined and default value of null is respected with refillAmount and refillInterval provided", async () => { 677 | const refillAmount = 10; // Arbitrary non-null value 678 | const refillInterval = 1000; 679 | const apiKey = await auth.api.createApiKey({ 680 | body: { 681 | refillAmount: refillAmount, 682 | refillInterval: refillInterval, 683 | userId: user.id, 684 | }, 685 | }); 686 | 687 | expect(apiKey).not.toBeNull(); 688 | expect(apiKey.remaining).toBeNull(); 689 | expect(apiKey.refillAmount).toBe(refillAmount); 690 | expect(apiKey.refillInterval).toBe(refillInterval); 691 | }); 692 | 693 | it("should create API key with invalid metadata", async () => { 694 | let result: { data: ApiKey | null; error: Err | null } = { 695 | data: null, 696 | error: null, 697 | }; 698 | try { 699 | const apiKey = await auth.api.createApiKey({ 700 | body: { 701 | metadata: "invalid", 702 | }, 703 | headers, 704 | }); 705 | result.data = apiKey; 706 | } catch (error: any) { 707 | result.error = error; 708 | } 709 | expect(result.data).toBeNull(); 710 | expect(result.error).toBeDefined(); 711 | expect(result.error?.status).toEqual("BAD_REQUEST"); 712 | expect(result.error?.body.message).toEqual( 713 | ERROR_CODES.INVALID_METADATA_TYPE, 714 | ); 715 | }); 716 | 717 | it("should create API key with valid metadata", async () => { 718 | const metadata = { 719 | test: "test", 720 | }; 721 | const apiKey = await auth.api.createApiKey({ 722 | body: { 723 | metadata: metadata, 724 | }, 725 | headers, 726 | }); 727 | 728 | expect(apiKey).not.toBeNull(); 729 | expect(apiKey.metadata).toEqual(metadata); 730 | 731 | const res = await auth.api.getApiKey({ 732 | query: { 733 | id: apiKey.id, 734 | }, 735 | headers, 736 | }); 737 | 738 | expect(res).not.toBeNull(); 739 | if (res) { 740 | expect(res.metadata).toEqual(metadata); 741 | } 742 | }); 743 | 744 | it("create API key's returned metadata should be an object", async () => { 745 | const metadata = { 746 | test: "test-123", 747 | }; 748 | const apiKey = await auth.api.createApiKey({ 749 | body: { 750 | metadata: metadata, 751 | }, 752 | headers, 753 | }); 754 | 755 | expect(apiKey).not.toBeNull(); 756 | expect(apiKey.metadata.test).toBeDefined(); 757 | expect(apiKey.metadata.test).toEqual(metadata.test); 758 | }); 759 | 760 | it("create API key with with metadata when metadata is disabled (should fail)", async () => { 761 | const { client, auth, signInWithTestUser } = await getTestInstance( 762 | { 763 | plugins: [ 764 | apiKey({ 765 | enableMetadata: false, 766 | }), 767 | ], 768 | }, 769 | { 770 | clientOptions: { 771 | plugins: [apiKeyClient()], 772 | }, 773 | }, 774 | ); 775 | const { headers } = await signInWithTestUser(); 776 | 777 | const metadata = { 778 | test: "test-123", 779 | }; 780 | const result: { data: ApiKey | null; error: Err | null } = { 781 | data: null, 782 | error: null, 783 | }; 784 | try { 785 | const apiKey = await auth.api.createApiKey({ 786 | body: { 787 | metadata: metadata, 788 | }, 789 | headers, 790 | }); 791 | result.data = apiKey; 792 | } catch (error: any) { 793 | result.error = error; 794 | } 795 | 796 | expect(result.data).toBeNull(); 797 | expect(result.error).toBeDefined(); 798 | expect(result.error?.status).toEqual("BAD_REQUEST"); 799 | expect(result.error?.body.message).toEqual(ERROR_CODES.METADATA_DISABLED); 800 | }); 801 | 802 | it("should have the first 6 chracaters of the key as the start property", async () => { 803 | const { data: apiKey } = await client.apiKey.create( 804 | {}, 805 | { headers: headers }, 806 | ); 807 | 808 | expect(apiKey?.start).toBeDefined(); 809 | expect(apiKey?.start?.length).toEqual(6); 810 | expect(apiKey?.start).toEqual(apiKey?.key?.substring(0, 6)); 811 | }); 812 | 813 | it("should have the start property as null if shouldStore is false", async () => { 814 | const { client, auth, signInWithTestUser } = await getTestInstance( 815 | { 816 | plugins: [ 817 | apiKey({ 818 | startingCharactersConfig: { 819 | shouldStore: false, 820 | }, 821 | }), 822 | ], 823 | }, 824 | { 825 | clientOptions: { 826 | plugins: [apiKeyClient()], 827 | }, 828 | }, 829 | ); 830 | const { headers } = await signInWithTestUser(); 831 | 832 | const { data: apiKey2 } = await client.apiKey.create( 833 | {}, 834 | { headers: headers }, 835 | ); 836 | 837 | expect(apiKey2?.start).toBeNull(); 838 | }); 839 | 840 | it("should use the defined charactersLength if provided", async () => { 841 | const customLength = 3; 842 | const { client, auth, signInWithTestUser } = await getTestInstance( 843 | { 844 | plugins: [ 845 | apiKey({ 846 | startingCharactersConfig: { 847 | shouldStore: true, 848 | charactersLength: customLength, 849 | }, 850 | }), 851 | ], 852 | }, 853 | { 854 | clientOptions: { 855 | plugins: [apiKeyClient()], 856 | }, 857 | }, 858 | ); 859 | const { headers } = await signInWithTestUser(); 860 | 861 | const { data: apiKey2 } = await client.apiKey.create( 862 | {}, 863 | { headers: headers }, 864 | ); 865 | 866 | expect(apiKey2?.start).toBeDefined(); 867 | expect(apiKey2?.start?.length).toEqual(customLength); 868 | expect(apiKey2?.start).toEqual(apiKey2?.key?.substring(0, customLength)); 869 | }); 870 | 871 | it("should fail to create API key with custom rate-limit options from client auth", async () => { 872 | const apiKey = await client.apiKey.create( 873 | { 874 | rateLimitMax: 15, 875 | }, 876 | { headers }, 877 | ); 878 | 879 | expect(apiKey.data).toBeNull(); 880 | expect(apiKey.error).toBeDefined(); 881 | expect(apiKey.error?.statusText).toEqual("BAD_REQUEST"); 882 | expect(apiKey.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY); 883 | 884 | const apiKey2 = await client.apiKey.create( 885 | { 886 | rateLimitTimeWindow: 1001, 887 | }, 888 | { headers }, 889 | ); 890 | 891 | expect(apiKey2.data).toBeNull(); 892 | expect(apiKey2.error).toBeDefined(); 893 | expect(apiKey2.error?.statusText).toEqual("BAD_REQUEST"); 894 | expect(apiKey2.error?.message).toEqual(ERROR_CODES.SERVER_ONLY_PROPERTY); 895 | }); 896 | 897 | it("should successfully apply custom rate-limit options on the newly created API key", async () => { 898 | const apiKey = await auth.api.createApiKey({ 899 | body: { 900 | rateLimitMax: 15, 901 | rateLimitTimeWindow: 1000, 902 | userId: user.id, 903 | }, 904 | }); 905 | 906 | expect(apiKey).not.toBeNull(); 907 | expect(apiKey?.rateLimitMax).toEqual(15); 908 | expect(apiKey?.rateLimitTimeWindow).toEqual(1000); 909 | }); 910 | 911 | // ========================================================================= 912 | // VERIFY API KEY 913 | // ========================================================================= 914 | 915 | it("verify API key without key and userId", async () => { 916 | const apiKey = await auth.api.verifyApiKey({ 917 | body: { 918 | key: firstApiKey.key, 919 | }, 920 | }); 921 | expect(apiKey.key).not.toBe(null); 922 | expect(apiKey.valid).toBe(true); 923 | }); 924 | 925 | it("verify API key with invalid key (should fail)", async () => { 926 | const apiKey = await auth.api.verifyApiKey({ 927 | body: { 928 | key: "invalid", 929 | }, 930 | }); 931 | expect(apiKey.valid).toBe(false); 932 | expect(apiKey.error?.code).toBe("KEY_NOT_FOUND"); 933 | }); 934 | 935 | let rateLimitedApiKey: ApiKey; 936 | 937 | const { 938 | client: rateLimitClient, 939 | auth: rateLimitAuth, 940 | signInWithTestUser: rateLimitTestUser, 941 | } = await getTestInstance( 942 | { 943 | plugins: [ 944 | apiKey({ 945 | rateLimit: { 946 | enabled: true, 947 | timeWindow: 1000, 948 | }, 949 | }), 950 | ], 951 | }, 952 | { 953 | clientOptions: { 954 | plugins: [apiKeyClient()], 955 | }, 956 | }, 957 | ); 958 | 959 | const { headers: rateLimitUserHeaders } = await rateLimitTestUser(); 960 | 961 | it("should fail to verify API key 20 times in a row due to rate-limit", async () => { 962 | const { data: apiKey2 } = await rateLimitClient.apiKey.create( 963 | {}, 964 | { headers: rateLimitUserHeaders }, 965 | ); 966 | if (!apiKey2) return; 967 | rateLimitedApiKey = apiKey2; 968 | for (let i = 0; i < 20; i++) { 969 | const response = await rateLimitAuth.api.verifyApiKey({ 970 | body: { 971 | key: apiKey2.key, 972 | }, 973 | headers: rateLimitUserHeaders, 974 | }); 975 | if (i >= 10) { 976 | expect(response.error?.code).toBe("RATE_LIMITED"); 977 | } else { 978 | expect(response.error).toBeNull(); 979 | } 980 | } 981 | }); 982 | 983 | it("should allow us to verify API key after rate-limit window has passed", async () => { 984 | vi.useFakeTimers(); 985 | await vi.advanceTimersByTimeAsync(1000); 986 | const response = await rateLimitAuth.api.verifyApiKey({ 987 | body: { 988 | key: rateLimitedApiKey.key, 989 | }, 990 | headers: rateLimitUserHeaders, 991 | }); 992 | expect(response.error).toBeNull(); 993 | expect(response?.valid).toBe(true); 994 | }); 995 | 996 | it("should check if verifying an API key's remaining count does go down", async () => { 997 | const remaining = 10; 998 | const { data: apiKey } = await client.apiKey.create( 999 | { 1000 | remaining: remaining, 1001 | }, 1002 | { headers: headers }, 1003 | ); 1004 | if (!apiKey) return; 1005 | const afterVerificationOnce = await auth.api.verifyApiKey({ 1006 | body: { 1007 | key: apiKey.key, 1008 | }, 1009 | headers, 1010 | }); 1011 | expect(afterVerificationOnce?.valid).toEqual(true); 1012 | expect(afterVerificationOnce?.key?.remaining).toEqual(remaining - 1); 1013 | const afterVerificationTwice = await auth.api.verifyApiKey({ 1014 | body: { 1015 | key: apiKey.key, 1016 | }, 1017 | headers, 1018 | }); 1019 | expect(afterVerificationTwice?.valid).toEqual(true); 1020 | expect(afterVerificationTwice?.key?.remaining).toEqual(remaining - 2); 1021 | }); 1022 | 1023 | it("should fail if the API key has no remaining", async () => { 1024 | const apiKey = await auth.api.createApiKey({ 1025 | body: { 1026 | remaining: 1, 1027 | userId: user.id, 1028 | }, 1029 | }); 1030 | if (!apiKey) return; 1031 | // run verify once to make the remaining count go down to 0 1032 | await auth.api.verifyApiKey({ 1033 | body: { 1034 | key: apiKey.key, 1035 | }, 1036 | headers, 1037 | }); 1038 | const afterVerification = await auth.api.verifyApiKey({ 1039 | body: { 1040 | key: apiKey.key, 1041 | }, 1042 | headers, 1043 | }); 1044 | expect(afterVerification.error?.code).toBe("USAGE_EXCEEDED"); 1045 | }); 1046 | 1047 | it("should fail if the API key is expired", async () => { 1048 | vi.useRealTimers(); 1049 | const { headers } = await signInWithTestUser(); 1050 | const apiKey2 = await client.apiKey.create( 1051 | { 1052 | expiresIn: 60 * 60 * 24, 1053 | }, 1054 | { headers: headers, throw: true }, 1055 | ); 1056 | vi.useFakeTimers(); 1057 | await vi.advanceTimersByTimeAsync(1000 * 60 * 60 * 24 * 2); 1058 | const afterVerification = await auth.api.verifyApiKey({ 1059 | body: { 1060 | key: apiKey2.key, 1061 | }, 1062 | headers, 1063 | }); 1064 | expect(afterVerification.error?.code).toEqual("KEY_EXPIRED"); 1065 | vi.useRealTimers(); 1066 | }); 1067 | 1068 | // ========================================================================= 1069 | // UPDATE API KEY 1070 | // ========================================================================= 1071 | 1072 | interface Err { 1073 | body: { 1074 | code: string | undefined; 1075 | message: string | undefined; 1076 | }; 1077 | status: string; 1078 | statusCode: string; 1079 | } 1080 | 1081 | it("should fail to update API key name without headers or userId", async () => { 1082 | let res: { data: ApiKey | null; error: Err | null } = { 1083 | data: null, 1084 | error: null, 1085 | }; 1086 | try { 1087 | const apiKey = await auth.api.updateApiKey({ 1088 | body: { 1089 | keyId: firstApiKey.id, 1090 | name: "test-api-key", 1091 | }, 1092 | }); 1093 | res.data = apiKey as ApiKey; 1094 | } catch (error: any) { 1095 | res.error = error; 1096 | } 1097 | expect(res.data).toBeNull(); 1098 | expect(res.error).toBeDefined(); 1099 | expect(res.error?.statusCode).toEqual(401); 1100 | expect(res.error?.status).toEqual("UNAUTHORIZED"); 1101 | expect(res.error?.body.message).toEqual(ERROR_CODES.UNAUTHORIZED_SESSION); 1102 | }); 1103 | 1104 | it("should update API key name with headers", async () => { 1105 | const newName = "Hello World"; 1106 | const apiKey = await auth.api.updateApiKey({ 1107 | body: { 1108 | keyId: firstApiKey.id, 1109 | name: newName, 1110 | }, 1111 | headers, 1112 | }); 1113 | expect(apiKey).toBeDefined(); 1114 | expect(apiKey.name).not.toEqual(firstApiKey.name); 1115 | expect(apiKey.name).toEqual(newName); 1116 | }); 1117 | 1118 | it("should fail to update API key name with a length larger than the allowed maximum", async () => { 1119 | let error: APIError | null = null; 1120 | await auth.api 1121 | .updateApiKey({ 1122 | body: { 1123 | keyId: firstApiKey.id, 1124 | name: "test-api-key-that-is-longer-than-the-allowed-maximum", 1125 | }, 1126 | headers, 1127 | }) 1128 | .catch((e) => { 1129 | if (e instanceof APIError) { 1130 | error = e; 1131 | expect(error?.status).toEqual("BAD_REQUEST"); 1132 | expect(error?.body?.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH); 1133 | } 1134 | }); 1135 | expect(error).not.toBeNull(); 1136 | }); 1137 | 1138 | it("should fail to update API key name with a length smaller than the allowed minimum", async () => { 1139 | let error: APIError | null = null; 1140 | await auth.api 1141 | .updateApiKey({ 1142 | body: { 1143 | keyId: firstApiKey.id, 1144 | name: "", 1145 | }, 1146 | headers, 1147 | }) 1148 | .catch((e) => { 1149 | if (e instanceof APIError) { 1150 | error = e; 1151 | expect(error?.status).toEqual("BAD_REQUEST"); 1152 | expect(error?.body?.message).toEqual(ERROR_CODES.INVALID_NAME_LENGTH); 1153 | } 1154 | }); 1155 | expect(error).not.toBeNull(); 1156 | }); 1157 | 1158 | it("should fail to update API key with no values to update", async () => { 1159 | let error: APIError | null = null; 1160 | await auth.api 1161 | .updateApiKey({ 1162 | body: { 1163 | keyId: firstApiKey.id, 1164 | }, 1165 | headers, 1166 | }) 1167 | .catch((e) => { 1168 | if (e instanceof APIError) { 1169 | error = e; 1170 | expect(error?.status).toEqual("BAD_REQUEST"); 1171 | expect(error?.body?.message).toEqual(ERROR_CODES.NO_VALUES_TO_UPDATE); 1172 | } 1173 | }); 1174 | expect(error).not.toBeNull(); 1175 | }); 1176 | 1177 | it("should update API key expiresIn value", async () => { 1178 | const expiresIn = 60 * 60 * 24 * 7; // 7 days 1179 | const expectedResult = new Date().getTime() + expiresIn; 1180 | const apiKey = await auth.api.updateApiKey({ 1181 | body: { 1182 | keyId: firstApiKey.id, 1183 | expiresIn: expiresIn, 1184 | }, 1185 | headers, 1186 | }); 1187 | expect(apiKey).not.toBeNull(); 1188 | expect(apiKey.expiresAt).toBeDefined(); 1189 | expect(apiKey.expiresAt?.getTime()).toBeGreaterThanOrEqual(expectedResult); 1190 | }); 1191 | 1192 | it("should fail to update expiresIn value if `disableCustomExpiresTime` is enabled", async () => { 1193 | const { client, auth, signInWithTestUser } = await getTestInstance( 1194 | { 1195 | plugins: [ 1196 | apiKey({ 1197 | keyExpiration: { 1198 | disableCustomExpiresTime: true, 1199 | }, 1200 | }), 1201 | ], 1202 | }, 1203 | { 1204 | clientOptions: { 1205 | plugins: [apiKeyClient()], 1206 | }, 1207 | }, 1208 | ); 1209 | const { headers } = await signInWithTestUser(); 1210 | 1211 | const { data: firstApiKey } = await client.apiKey.create({}, { headers }); 1212 | 1213 | if (!firstApiKey) return; 1214 | 1215 | let result: { data: Partial<ApiKey> | null; error: Err | null } = { 1216 | data: null, 1217 | error: null, 1218 | }; 1219 | try { 1220 | const apiKey = await auth.api.updateApiKey({ 1221 | body: { 1222 | keyId: firstApiKey.id, 1223 | expiresIn: 1000 * 60 * 60 * 24 * 7, // 7 days 1224 | }, 1225 | headers, 1226 | }); 1227 | result.data = apiKey; 1228 | } catch (error: any) { 1229 | result.error = error; 1230 | } 1231 | expect(result.data).toBeNull(); 1232 | expect(result.error).toBeDefined(); 1233 | expect(result.error?.status).toEqual("BAD_REQUEST"); 1234 | expect(result.error?.body.message).toEqual( 1235 | ERROR_CODES.KEY_DISABLED_EXPIRATION, 1236 | ); 1237 | }); 1238 | 1239 | it("should fail to update expiresIn value if it's smaller than the allowed minimum", async () => { 1240 | const { client, auth, signInWithTestUser } = await getTestInstance( 1241 | { 1242 | plugins: [ 1243 | apiKey({ 1244 | keyExpiration: { 1245 | minExpiresIn: 1, 1246 | }, 1247 | }), 1248 | ], 1249 | }, 1250 | { 1251 | clientOptions: { 1252 | plugins: [apiKeyClient()], 1253 | }, 1254 | }, 1255 | ); 1256 | const { headers } = await signInWithTestUser(); 1257 | 1258 | const { data: firstApiKey } = await client.apiKey.create({}, { headers }); 1259 | 1260 | if (!firstApiKey) return; 1261 | 1262 | let result: { data: Partial<ApiKey> | null; error: Err | null } = { 1263 | data: null, 1264 | error: null, 1265 | }; 1266 | try { 1267 | const apiKey = await auth.api.updateApiKey({ 1268 | body: { 1269 | keyId: firstApiKey.id, 1270 | expiresIn: 1, 1271 | }, 1272 | headers, 1273 | }); 1274 | result.data = apiKey; 1275 | } catch (error: any) { 1276 | result.error = error; 1277 | } 1278 | expect(result.data).toBeNull(); 1279 | expect(result.error).toBeDefined(); 1280 | expect(result.error?.status).toEqual("BAD_REQUEST"); 1281 | expect(result.error?.body.message).toEqual( 1282 | ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL, 1283 | ); 1284 | }); 1285 | 1286 | it("should fail to update expiresIn value if it's larger than the allowed maximum", async () => { 1287 | const { client, auth, signInWithTestUser } = await getTestInstance( 1288 | { 1289 | plugins: [ 1290 | apiKey({ 1291 | keyExpiration: { 1292 | maxExpiresIn: 1, 1293 | }, 1294 | }), 1295 | ], 1296 | }, 1297 | { 1298 | clientOptions: { 1299 | plugins: [apiKeyClient()], 1300 | }, 1301 | }, 1302 | ); 1303 | const { headers } = await signInWithTestUser(); 1304 | 1305 | const { data: firstApiKey } = await client.apiKey.create({}, { headers }); 1306 | 1307 | if (!firstApiKey) return; 1308 | 1309 | let result: { data: Partial<ApiKey> | null; error: Err | null } = { 1310 | data: null, 1311 | error: null, 1312 | }; 1313 | try { 1314 | const apiKey = await auth.api.updateApiKey({ 1315 | body: { 1316 | keyId: firstApiKey.id, 1317 | expiresIn: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years 1318 | }, 1319 | headers, 1320 | }); 1321 | result.data = apiKey; 1322 | } catch (error: any) { 1323 | result.error = error; 1324 | } 1325 | expect(result.data).toBeNull(); 1326 | expect(result.error).toBeDefined(); 1327 | expect(result.error?.status).toEqual("BAD_REQUEST"); 1328 | expect(result.error?.body.message).toEqual( 1329 | ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE, 1330 | ); 1331 | }); 1332 | 1333 | it("should update API key remaining count", async () => { 1334 | const remaining = 100; 1335 | const apiKey = await auth.api.updateApiKey({ 1336 | body: { 1337 | keyId: firstApiKey.id, 1338 | remaining: remaining, 1339 | userId: user.id, 1340 | }, 1341 | }); 1342 | 1343 | expect(apiKey).not.toBeNull(); 1344 | expect(apiKey.remaining).toEqual(remaining); 1345 | }); 1346 | 1347 | it("should fail update the refillInterval value since it requires refillAmount as well", async () => { 1348 | let result: { data: Partial<ApiKey> | null; error: Err | null } = { 1349 | data: null, 1350 | error: null, 1351 | }; 1352 | try { 1353 | const apiKey = await auth.api.updateApiKey({ 1354 | body: { 1355 | keyId: firstApiKey.id, 1356 | refillInterval: 1000, 1357 | userId: user.id, 1358 | }, 1359 | }); 1360 | result.data = apiKey; 1361 | } catch (error: any) { 1362 | result.error = error; 1363 | } 1364 | expect(result.data).toBeNull(); 1365 | expect(result.error).toBeDefined(); 1366 | expect(result.error?.status).toEqual("BAD_REQUEST"); 1367 | expect(result.error?.body.message).toEqual( 1368 | ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED, 1369 | ); 1370 | }); 1371 | 1372 | it("should fail update the refillAmount value since it requires refillInterval as well", async () => { 1373 | let result: { data: Partial<ApiKey> | null; error: Err | null } = { 1374 | data: null, 1375 | error: null, 1376 | }; 1377 | try { 1378 | const apiKey = await auth.api.updateApiKey({ 1379 | body: { 1380 | keyId: firstApiKey.id, 1381 | refillAmount: 10, 1382 | userId: user.id, 1383 | }, 1384 | }); 1385 | result.data = apiKey; 1386 | } catch (error: any) { 1387 | result.error = error; 1388 | } 1389 | expect(result.data).toBeNull(); 1390 | expect(result.error).toBeDefined(); 1391 | expect(result.error?.status).toEqual("BAD_REQUEST"); 1392 | expect(result.error?.body.message).toEqual( 1393 | ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED, 1394 | ); 1395 | }); 1396 | 1397 | it("should update the refillInterval and refillAmount value", async () => { 1398 | const refillInterval = 10000; 1399 | const refillAmount = 100; 1400 | const apiKey = await auth.api.updateApiKey({ 1401 | body: { 1402 | keyId: firstApiKey.id, 1403 | refillInterval: refillInterval, 1404 | refillAmount: refillAmount, 1405 | userId: user.id, 1406 | }, 1407 | }); 1408 | 1409 | expect(apiKey).not.toBeNull(); 1410 | expect(apiKey.refillInterval).toEqual(refillInterval); 1411 | expect(apiKey.refillAmount).toEqual(refillAmount); 1412 | }); 1413 | 1414 | it("should update API key enable value", async () => { 1415 | const newValue = false; 1416 | const apiKey = await auth.api.updateApiKey({ 1417 | body: { 1418 | keyId: firstApiKey.id, 1419 | enabled: newValue, 1420 | userId: user.id, 1421 | }, 1422 | }); 1423 | 1424 | expect(apiKey).not.toBeNull(); 1425 | expect(apiKey.enabled).toEqual(newValue); 1426 | }); 1427 | 1428 | it("should fail to update metadata with invalid metadata type", async () => { 1429 | let result: { data: Partial<ApiKey> | null; error: Err | null } = { 1430 | data: null, 1431 | error: null, 1432 | }; 1433 | try { 1434 | const apiKey = await auth.api.updateApiKey({ 1435 | body: { 1436 | keyId: firstApiKey.id, 1437 | metadata: "invalid", 1438 | userId: user.id, 1439 | }, 1440 | }); 1441 | result.data = apiKey; 1442 | } catch (error: any) { 1443 | result.error = error; 1444 | } 1445 | expect(result.data).toBeNull(); 1446 | expect(result.error).toBeDefined(); 1447 | expect(result.error?.status).toEqual("BAD_REQUEST"); 1448 | expect(result.error?.body.message).toEqual( 1449 | ERROR_CODES.INVALID_METADATA_TYPE, 1450 | ); 1451 | }); 1452 | 1453 | it("should update metadata with valid metadata type", async () => { 1454 | const metadata = { 1455 | test: "test-123", 1456 | }; 1457 | const apiKey = await auth.api.updateApiKey({ 1458 | body: { 1459 | keyId: firstApiKey.id, 1460 | metadata: metadata, 1461 | userId: user.id, 1462 | }, 1463 | }); 1464 | 1465 | expect(apiKey).not.toBeNull(); 1466 | expect(apiKey.metadata).toEqual(metadata); 1467 | }); 1468 | 1469 | it("update API key's returned metadata should be an object", async () => { 1470 | const metadata = { 1471 | test: "test-12345", 1472 | }; 1473 | const apiKey = await auth.api.updateApiKey({ 1474 | body: { 1475 | keyId: firstApiKey.id, 1476 | metadata: metadata, 1477 | userId: user.id, 1478 | }, 1479 | }); 1480 | 1481 | expect(apiKey).not.toBeNull(); 1482 | expect(apiKey.metadata?.test).toBeDefined(); 1483 | expect(apiKey.metadata?.test).toEqual(metadata.test); 1484 | }); 1485 | 1486 | // ========================================================================= 1487 | // GET API KEY 1488 | // ========================================================================= 1489 | 1490 | it("should get an API key by id", async () => { 1491 | const apiKey = await client.apiKey.get({ 1492 | query: { 1493 | id: firstApiKey.id, 1494 | }, 1495 | fetchOptions: { 1496 | headers, 1497 | }, 1498 | }); 1499 | expect(apiKey.data).not.toBeNull(); 1500 | expect(apiKey.data?.id).toBe(firstApiKey.id); 1501 | }); 1502 | 1503 | it("should fail to get an API key by ID that doesn't exist", async () => { 1504 | const result = await client.apiKey.get( 1505 | { 1506 | query: { 1507 | id: "invalid", 1508 | }, 1509 | }, 1510 | { headers }, 1511 | ); 1512 | expect(result.data).toBeNull(); 1513 | expect(result.error).toBeDefined(); 1514 | expect(result.error?.status).toEqual(404); 1515 | }); 1516 | 1517 | it("should successfully receive an object metadata from an API key", async () => { 1518 | const apiKey = await client.apiKey.get( 1519 | { 1520 | query: { 1521 | id: firstApiKey.id, 1522 | }, 1523 | }, 1524 | { 1525 | headers, 1526 | }, 1527 | ); 1528 | expect(apiKey).not.toBeNull(); 1529 | expect(apiKey.data?.metadata).toBeDefined(); 1530 | expect(apiKey.data?.metadata).toBeInstanceOf(Object); 1531 | }); 1532 | 1533 | // ========================================================================= 1534 | // LIST API KEY 1535 | // ========================================================================= 1536 | 1537 | it("should fail to list API keys without headers", async () => { 1538 | let result: { data: Partial<ApiKey>[] | null; error: Err | null } = { 1539 | data: null, 1540 | error: null, 1541 | }; 1542 | try { 1543 | const apiKey = await auth.api.listApiKeys({}); 1544 | result.data = apiKey; 1545 | } catch (error: any) { 1546 | result.error = error; 1547 | } 1548 | 1549 | expect(result.data).toBeNull(); 1550 | expect(result.error).toBeDefined(); 1551 | expect(result.error?.status).toEqual("UNAUTHORIZED"); 1552 | }); 1553 | 1554 | it("should list API keys with headers", async () => { 1555 | const apiKeys = await auth.api.listApiKeys({ 1556 | headers, 1557 | }); 1558 | 1559 | expect(apiKeys).not.toBeNull(); 1560 | expect(apiKeys.length).toBeGreaterThan(0); 1561 | }); 1562 | 1563 | it("should list API keys with metadata as an object", async () => { 1564 | const apiKeys = await auth.api.listApiKeys({ 1565 | headers, 1566 | }); 1567 | 1568 | expect(apiKeys).not.toBeNull(); 1569 | expect(apiKeys.length).toBeGreaterThan(0); 1570 | apiKeys.map((apiKey) => { 1571 | if (apiKey.metadata) { 1572 | expect(apiKey.metadata).toBeInstanceOf(Object); 1573 | } 1574 | }); 1575 | }); 1576 | 1577 | // ========================================================================= 1578 | // Sessions from API keys 1579 | // ========================================================================= 1580 | 1581 | it("should get session from an API key", async () => { 1582 | const { client, auth, signInWithTestUser } = await getTestInstance( 1583 | { 1584 | plugins: [ 1585 | apiKey({ 1586 | enableSessionForAPIKeys: true, 1587 | }), 1588 | ], 1589 | }, 1590 | { 1591 | clientOptions: { 1592 | plugins: [apiKeyClient()], 1593 | }, 1594 | }, 1595 | ); 1596 | 1597 | const { headers: userHeaders } = await signInWithTestUser(); 1598 | 1599 | const { data: apiKey2 } = await client.apiKey.create( 1600 | {}, 1601 | { headers: userHeaders }, 1602 | ); 1603 | if (!apiKey2) return; 1604 | const headers = new Headers(); 1605 | headers.set("x-api-key", apiKey2.key); 1606 | 1607 | const session = await auth.api.getSession({ 1608 | headers: headers, 1609 | }); 1610 | 1611 | expect(session?.session).toBeDefined(); 1612 | }); 1613 | 1614 | // ========================================================================= 1615 | // DELETE API KEY 1616 | // ========================================================================= 1617 | 1618 | it("should fail to delete an API key by ID without headers", async () => { 1619 | let result: { data: { success: boolean } | null; error: Err | null } = { 1620 | data: null, 1621 | error: null, 1622 | }; 1623 | try { 1624 | const apiKey = await auth.api.deleteApiKey({ 1625 | body: { 1626 | keyId: firstApiKey.id, 1627 | }, 1628 | }); 1629 | result.data = apiKey; 1630 | } catch (error: any) { 1631 | result.error = error; 1632 | } 1633 | 1634 | expect(result.data).toBeNull(); 1635 | expect(result.error).toBeDefined(); 1636 | expect(result.error?.status).toEqual("UNAUTHORIZED"); 1637 | }); 1638 | 1639 | it("should delete an API key by ID with headers", async () => { 1640 | const apiKey = await auth.api.deleteApiKey({ 1641 | body: { 1642 | keyId: firstApiKey.id, 1643 | }, 1644 | headers, 1645 | }); 1646 | 1647 | expect(apiKey).not.toBeNull(); 1648 | expect(apiKey.success).toEqual(true); 1649 | }); 1650 | 1651 | it("should delete an API key by ID with headers using auth-client", async () => { 1652 | const newApiKey = await client.apiKey.create({}, { headers: headers }); 1653 | if (!newApiKey.data) return; 1654 | 1655 | const apiKey = await client.apiKey.delete( 1656 | { 1657 | keyId: newApiKey.data.id, 1658 | }, 1659 | { headers }, 1660 | ); 1661 | 1662 | if (!apiKey.data?.success) { 1663 | console.log(apiKey.error); 1664 | } 1665 | 1666 | expect(apiKey).not.toBeNull(); 1667 | expect(apiKey.data?.success).toEqual(true); 1668 | }); 1669 | 1670 | it("should fail to delete an API key by ID that doesn't exist", async () => { 1671 | let result: { data: { success: boolean } | null; error: Err | null } = { 1672 | data: null, 1673 | error: null, 1674 | }; 1675 | try { 1676 | const apiKey = await auth.api.deleteApiKey({ 1677 | body: { 1678 | keyId: "invalid", 1679 | }, 1680 | headers, 1681 | }); 1682 | result.data = apiKey; 1683 | } catch (error: any) { 1684 | result.error = error; 1685 | } 1686 | expect(result.data).toBeNull(); 1687 | expect(result.error).toBeDefined(); 1688 | expect(result.error?.status).toEqual("NOT_FOUND"); 1689 | expect(result.error?.body.message).toEqual(ERROR_CODES.KEY_NOT_FOUND); 1690 | }); 1691 | 1692 | it("should create an API key with permissions", async () => { 1693 | const permissions = { 1694 | files: ["read", "write"], 1695 | users: ["read"], 1696 | }; 1697 | 1698 | const apiKey = await auth.api.createApiKey({ 1699 | body: { 1700 | permissions, 1701 | userId: user.id, 1702 | }, 1703 | }); 1704 | expect(apiKey).not.toBeNull(); 1705 | expect(apiKey.permissions).toEqual(permissions); 1706 | }); 1707 | 1708 | it("should have permissions as an object from getApiKey", async () => { 1709 | const permissions = { 1710 | files: ["read", "write"], 1711 | users: ["read"], 1712 | }; 1713 | 1714 | const apiKey = await auth.api.createApiKey({ 1715 | body: { 1716 | permissions, 1717 | userId: user.id, 1718 | }, 1719 | }); 1720 | 1721 | const apiKeyResults = await auth.api.getApiKey({ 1722 | query: { 1723 | id: apiKey.id, 1724 | }, 1725 | headers, 1726 | }); 1727 | 1728 | expect(apiKeyResults).not.toBeNull(); 1729 | expect(apiKeyResults.permissions).toEqual(permissions); 1730 | }); 1731 | 1732 | it("should have permissions as an object from verifyApiKey", async () => { 1733 | const permissions = { 1734 | files: ["read", "write"], 1735 | users: ["read"], 1736 | }; 1737 | 1738 | const apiKey = await auth.api.createApiKey({ 1739 | body: { 1740 | permissions, 1741 | userId: user.id, 1742 | }, 1743 | }); 1744 | const apiKeyResults = await auth.api.verifyApiKey({ 1745 | body: { 1746 | key: apiKey.key, 1747 | permissions: { 1748 | files: ["read"], 1749 | }, 1750 | }, 1751 | headers, 1752 | }); 1753 | 1754 | expect(apiKeyResults).not.toBeNull(); 1755 | expect(apiKeyResults.key?.permissions).toEqual(permissions); 1756 | }); 1757 | 1758 | it("should create an API key with default permissions", async () => { 1759 | const apiKey = await auth.api.createApiKey({ 1760 | body: { 1761 | userId: user.id, 1762 | }, 1763 | }); 1764 | expect(apiKey).not.toBeNull(); 1765 | expect(apiKey.permissions).toEqual({ 1766 | files: ["read"], 1767 | }); 1768 | }); 1769 | 1770 | it("should have valid metadata from key verification results", async () => { 1771 | const metadata = { 1772 | test: "hello-world-123", 1773 | }; 1774 | const apiKey = await auth.api.createApiKey({ 1775 | body: { 1776 | userId: user.id, 1777 | metadata: metadata, 1778 | }, 1779 | headers, 1780 | }); 1781 | 1782 | expect(apiKey).not.toBeNull(); 1783 | if (apiKey) { 1784 | const result = await auth.api.verifyApiKey({ 1785 | body: { 1786 | key: apiKey.key, 1787 | }, 1788 | headers, 1789 | }); 1790 | 1791 | expect(result.valid).toBe(true); 1792 | expect(result.error).toBeNull(); 1793 | expect(result.key?.metadata).toEqual(metadata); 1794 | } 1795 | }); 1796 | 1797 | it("should verify an API key with matching permissions", async () => { 1798 | const permissions = { 1799 | files: ["read", "write"], 1800 | users: ["read"], 1801 | }; 1802 | 1803 | const apiKey = await auth.api.createApiKey({ 1804 | body: { 1805 | permissions, 1806 | userId: user.id, 1807 | }, 1808 | }); 1809 | 1810 | const result = await auth.api.verifyApiKey({ 1811 | body: { 1812 | key: apiKey.key, 1813 | permissions: { 1814 | files: ["read"], 1815 | }, 1816 | }, 1817 | }); 1818 | 1819 | expect(result.valid).toBe(true); 1820 | expect(result.error).toBeNull(); 1821 | expect(result.key?.permissions).toEqual(permissions); 1822 | }); 1823 | 1824 | it("should fail to verify an API key with non-matching permissions", async () => { 1825 | const permissions = { 1826 | files: ["read"], 1827 | users: ["read"], 1828 | }; 1829 | 1830 | const apiKey = await auth.api.createApiKey({ 1831 | body: { 1832 | permissions, 1833 | userId: user.id, 1834 | }, 1835 | }); 1836 | 1837 | const result = await auth.api.verifyApiKey({ 1838 | body: { 1839 | key: apiKey.key, 1840 | permissions: { 1841 | files: ["write"], 1842 | }, 1843 | }, 1844 | }); 1845 | 1846 | expect(result.valid).toBe(false); 1847 | expect(result.error?.code).toBe("KEY_NOT_FOUND"); 1848 | }); 1849 | 1850 | it("should fail to verify when required permissions are specified but API key has no permissions", async () => { 1851 | const apiKey = await auth.api.createApiKey({ 1852 | body: { 1853 | userId: user.id, 1854 | }, 1855 | }); 1856 | 1857 | const result = await auth.api.verifyApiKey({ 1858 | body: { 1859 | key: apiKey.key, 1860 | permissions: { 1861 | files: ["write"], 1862 | }, 1863 | }, 1864 | }); 1865 | 1866 | expect(result.valid).toBe(false); 1867 | expect(result.error?.code).toBe("KEY_NOT_FOUND"); 1868 | }); 1869 | 1870 | it("should update an API key with permissions", async () => { 1871 | const permissions = { 1872 | files: ["read", "write"], 1873 | users: ["read"], 1874 | }; 1875 | const createdApiKey = await auth.api.createApiKey({ 1876 | body: { 1877 | userId: user.id, 1878 | }, 1879 | }); 1880 | expect(createdApiKey.permissions).not.toEqual(permissions); 1881 | const apiKey = await auth.api.updateApiKey({ 1882 | body: { 1883 | keyId: createdApiKey.id, 1884 | permissions, 1885 | userId: user.id, 1886 | }, 1887 | }); 1888 | expect(apiKey).not.toBeNull(); 1889 | expect(apiKey.permissions).toEqual(permissions); 1890 | }); 1891 | 1892 | it("should refill API key credits after refill interval (milliseconds)", async () => { 1893 | vi.useRealTimers(); 1894 | 1895 | const refillInterval = 3600000; // 1 hour in milliseconds 1896 | const refillAmount = 5; 1897 | const initialRemaining = 2; 1898 | 1899 | const apiKey = await auth.api.createApiKey({ 1900 | body: { 1901 | userId: user.id, 1902 | remaining: initialRemaining, 1903 | refillInterval: refillInterval, 1904 | refillAmount: refillAmount, 1905 | }, 1906 | }); 1907 | 1908 | let result = await auth.api.verifyApiKey({ 1909 | body: { 1910 | key: apiKey.key, 1911 | }, 1912 | }); 1913 | expect(result.valid).toBe(true); 1914 | expect(result.key?.remaining).toBe(initialRemaining - 1); 1915 | 1916 | result = await auth.api.verifyApiKey({ 1917 | body: { 1918 | key: apiKey.key, 1919 | }, 1920 | }); 1921 | expect(result.valid).toBe(true); 1922 | expect(result.key?.remaining).toBe(0); 1923 | 1924 | result = await auth.api.verifyApiKey({ 1925 | body: { 1926 | key: apiKey.key, 1927 | }, 1928 | }); 1929 | expect(result.valid).toBe(false); 1930 | expect(result.error?.code).toBe("USAGE_EXCEEDED"); 1931 | 1932 | vi.useFakeTimers(); 1933 | await vi.advanceTimersByTimeAsync(refillInterval + 1000); 1934 | 1935 | result = await auth.api.verifyApiKey({ 1936 | body: { 1937 | key: apiKey.key, 1938 | }, 1939 | }); 1940 | expect(result.valid).toBe(true); 1941 | expect(result.key?.remaining).toBe(refillAmount - 1); 1942 | 1943 | vi.useRealTimers(); 1944 | }); 1945 | 1946 | it("should not refill API key credits before refill interval expires", async () => { 1947 | vi.useRealTimers(); 1948 | 1949 | const refillInterval = 86400000; // 24 hours in milliseconds 1950 | const refillAmount = 10; 1951 | const initialRemaining = 1; 1952 | 1953 | const apiKey = await auth.api.createApiKey({ 1954 | body: { 1955 | userId: user.id, 1956 | remaining: initialRemaining, 1957 | refillInterval: refillInterval, 1958 | refillAmount: refillAmount, 1959 | }, 1960 | }); 1961 | 1962 | let result = await auth.api.verifyApiKey({ 1963 | body: { 1964 | key: apiKey.key, 1965 | }, 1966 | }); 1967 | expect(result.valid).toBe(true); 1968 | expect(result.key?.remaining).toBe(0); 1969 | 1970 | vi.useFakeTimers(); 1971 | await vi.advanceTimersByTimeAsync(refillInterval / 2); // Only advance half the interval 1972 | 1973 | result = await auth.api.verifyApiKey({ 1974 | body: { 1975 | key: apiKey.key, 1976 | }, 1977 | }); 1978 | expect(result.valid).toBe(false); 1979 | expect(result.error?.code).toBe("USAGE_EXCEEDED"); 1980 | 1981 | await vi.advanceTimersByTimeAsync(refillInterval / 2 + 1000); 1982 | 1983 | result = await auth.api.verifyApiKey({ 1984 | body: { 1985 | key: apiKey.key, 1986 | }, 1987 | }); 1988 | expect(result.valid).toBe(true); 1989 | expect(result.key?.remaining).toBe(refillAmount - 1); 1990 | 1991 | vi.useRealTimers(); 1992 | }); 1993 | 1994 | it("should handle multiple refill cycles correctly", async () => { 1995 | vi.useRealTimers(); 1996 | 1997 | const refillInterval = 3600000; // 1 hour in milliseconds 1998 | const refillAmount = 3; 1999 | 2000 | const apiKey = await auth.api.createApiKey({ 2001 | body: { 2002 | userId: user.id, 2003 | remaining: 1, 2004 | refillInterval: refillInterval, 2005 | refillAmount: refillAmount, 2006 | }, 2007 | }); 2008 | 2009 | let result = await auth.api.verifyApiKey({ 2010 | body: { 2011 | key: apiKey.key, 2012 | }, 2013 | }); 2014 | expect(result.valid).toBe(true); 2015 | expect(result.key?.remaining).toBe(0); 2016 | 2017 | vi.useFakeTimers(); 2018 | 2019 | await vi.advanceTimersByTimeAsync(refillInterval + 1000); 2020 | result = await auth.api.verifyApiKey({ 2021 | body: { 2022 | key: apiKey.key, 2023 | }, 2024 | }); 2025 | expect(result.valid).toBe(true); 2026 | expect(result.key?.remaining).toBe(refillAmount - 1); 2027 | 2028 | for (let i = 0; i < refillAmount - 1; i++) { 2029 | result = await auth.api.verifyApiKey({ 2030 | body: { 2031 | key: apiKey.key, 2032 | }, 2033 | }); 2034 | expect(result.valid).toBe(true); 2035 | } 2036 | 2037 | result = await auth.api.verifyApiKey({ 2038 | body: { 2039 | key: apiKey.key, 2040 | }, 2041 | }); 2042 | expect(result.valid).toBe(false); 2043 | expect(result.error?.code).toBe("USAGE_EXCEEDED"); 2044 | 2045 | await vi.advanceTimersByTimeAsync(refillInterval + 1000); 2046 | result = await auth.api.verifyApiKey({ 2047 | body: { 2048 | key: apiKey.key, 2049 | }, 2050 | }); 2051 | expect(result.valid).toBe(true); 2052 | expect(result.key?.remaining).toBe(refillAmount - 1); 2053 | 2054 | vi.useRealTimers(); 2055 | }); 2056 | }); 2057 | ```