#
tokens: 32566/50000 1/1099 files (page 59/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 59 of 69. 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
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   └── 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
│   │   └── vitest.setup.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/oidc-provider/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import * as z from "zod";
   2 | import { SignJWT } from "jose";
   3 | import { APIError, getSessionFromCtx, sessionMiddleware } from "../../api";
   4 | import {
   5 | 	createAuthEndpoint,
   6 | 	createAuthMiddleware,
   7 | } from "@better-auth/core/api";
   8 | import type { BetterAuthPlugin } from "@better-auth/core";
   9 | import {
  10 | 	generateRandomString,
  11 | 	symmetricDecrypt,
  12 | 	symmetricEncrypt,
  13 | } from "../../crypto";
  14 | import { schema } from "./schema";
  15 | import type {
  16 | 	Client,
  17 | 	CodeVerificationValue,
  18 | 	OAuthAccessToken,
  19 | 	OIDCMetadata,
  20 | 	OIDCOptions,
  21 | } from "./types";
  22 | import { authorize } from "./authorize";
  23 | import { parseSetCookieHeader } from "../../cookies";
  24 | import { createHash } from "@better-auth/utils/hash";
  25 | import { base64 } from "@better-auth/utils/base64";
  26 | import { getJwtToken } from "../jwt/sign";
  27 | import type { jwt } from "../jwt";
  28 | import { defaultClientSecretHasher } from "./utils";
  29 | import { mergeSchema } from "../../db";
  30 | import type { GenericEndpointContext } from "@better-auth/core";
  31 | 
  32 | const getJwtPlugin = (ctx: GenericEndpointContext) => {
  33 | 	return ctx.context.options.plugins?.find(
  34 | 		(plugin) => plugin.id === "jwt",
  35 | 	) as ReturnType<typeof jwt>;
  36 | };
  37 | 
  38 | /**
  39 |  * Get a client by ID, checking trusted clients first, then database
  40 |  */
  41 | export async function getClient(
  42 | 	clientId: string,
  43 | 	adapter: any,
  44 | 	trustedClients: (Client & { skipConsent?: boolean })[] = [],
  45 | ): Promise<(Client & { skipConsent?: boolean }) | null> {
  46 | 	const trustedClient = trustedClients.find(
  47 | 		(client) => client.clientId === clientId,
  48 | 	);
  49 | 	if (trustedClient) {
  50 | 		return trustedClient;
  51 | 	}
  52 | 	const dbClient = await adapter
  53 | 		.findOne({
  54 | 			model: "oauthApplication",
  55 | 			where: [{ field: "clientId", value: clientId }],
  56 | 		})
  57 | 		.then((res: Record<string, any> | null) => {
  58 | 			if (!res) {
  59 | 				return null;
  60 | 			}
  61 | 			return {
  62 | 				...res,
  63 | 				redirectURLs: (res.redirectURLs ?? "").split(","),
  64 | 				metadata: res.metadata ? JSON.parse(res.metadata) : {},
  65 | 			} as Client;
  66 | 		});
  67 | 
  68 | 	return dbClient;
  69 | }
  70 | 
  71 | export const getMetadata = (
  72 | 	ctx: GenericEndpointContext,
  73 | 	options?: OIDCOptions,
  74 | ): OIDCMetadata => {
  75 | 	const jwtPlugin = getJwtPlugin(ctx);
  76 | 	const issuer =
  77 | 		jwtPlugin && jwtPlugin.options?.jwt && jwtPlugin.options.jwt.issuer
  78 | 			? jwtPlugin.options.jwt.issuer
  79 | 			: (ctx.context.options.baseURL as string);
  80 | 	const baseURL = ctx.context.baseURL;
  81 | 	const supportedAlgs = options?.useJWTPlugin
  82 | 		? ["RS256", "EdDSA", "none"]
  83 | 		: ["HS256", "none"];
  84 | 	return {
  85 | 		issuer,
  86 | 		authorization_endpoint: `${baseURL}/oauth2/authorize`,
  87 | 		token_endpoint: `${baseURL}/oauth2/token`,
  88 | 		userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
  89 | 		jwks_uri: `${baseURL}/jwks`,
  90 | 		registration_endpoint: `${baseURL}/oauth2/register`,
  91 | 		scopes_supported: ["openid", "profile", "email", "offline_access"],
  92 | 		response_types_supported: ["code"],
  93 | 		response_modes_supported: ["query"],
  94 | 		grant_types_supported: ["authorization_code", "refresh_token"],
  95 | 		acr_values_supported: [
  96 | 			"urn:mace:incommon:iap:silver",
  97 | 			"urn:mace:incommon:iap:bronze",
  98 | 		],
  99 | 		subject_types_supported: ["public"],
 100 | 		id_token_signing_alg_values_supported: supportedAlgs,
 101 | 		token_endpoint_auth_methods_supported: [
 102 | 			"client_secret_basic",
 103 | 			"client_secret_post",
 104 | 			"none",
 105 | 		],
 106 | 		code_challenge_methods_supported: ["S256"],
 107 | 		claims_supported: [
 108 | 			"sub",
 109 | 			"iss",
 110 | 			"aud",
 111 | 			"exp",
 112 | 			"nbf",
 113 | 			"iat",
 114 | 			"jti",
 115 | 			"email",
 116 | 			"email_verified",
 117 | 			"name",
 118 | 		],
 119 | 		...options?.metadata,
 120 | 	};
 121 | };
 122 | 
 123 | /**
 124 |  * OpenID Connect (OIDC) plugin for Better Auth. This plugin implements the
 125 |  * authorization code flow and the token exchange flow. It also implements the
 126 |  * userinfo endpoint.
 127 |  *
 128 |  * @param options - The options for the OIDC plugin.
 129 |  * @returns A Better Auth plugin.
 130 |  */
 131 | export const oidcProvider = (options: OIDCOptions) => {
 132 | 	const modelName = {
 133 | 		oauthClient: "oauthApplication",
 134 | 		oauthAccessToken: "oauthAccessToken",
 135 | 		oauthConsent: "oauthConsent",
 136 | 	};
 137 | 
 138 | 	const opts = {
 139 | 		codeExpiresIn: 600,
 140 | 		defaultScope: "openid",
 141 | 		accessTokenExpiresIn: 3600,
 142 | 		refreshTokenExpiresIn: 604800,
 143 | 		allowPlainCodeChallengeMethod: true,
 144 | 		storeClientSecret: "plain" as const,
 145 | 		...options,
 146 | 		scopes: [
 147 | 			"openid",
 148 | 			"profile",
 149 | 			"email",
 150 | 			"offline_access",
 151 | 			...(options?.scopes || []),
 152 | 		],
 153 | 	};
 154 | 
 155 | 	const trustedClients = options.trustedClients || [];
 156 | 
 157 | 	/**
 158 | 	 * Store client secret according to the configured storage method
 159 | 	 */
 160 | 	async function storeClientSecret(
 161 | 		ctx: GenericEndpointContext,
 162 | 		clientSecret: string,
 163 | 	) {
 164 | 		if (opts.storeClientSecret === "encrypted") {
 165 | 			return await symmetricEncrypt({
 166 | 				key: ctx.context.secret,
 167 | 				data: clientSecret,
 168 | 			});
 169 | 		}
 170 | 		if (opts.storeClientSecret === "hashed") {
 171 | 			return await defaultClientSecretHasher(clientSecret);
 172 | 		}
 173 | 		if (
 174 | 			typeof opts.storeClientSecret === "object" &&
 175 | 			"hash" in opts.storeClientSecret
 176 | 		) {
 177 | 			return await opts.storeClientSecret.hash(clientSecret);
 178 | 		}
 179 | 		if (
 180 | 			typeof opts.storeClientSecret === "object" &&
 181 | 			"encrypt" in opts.storeClientSecret
 182 | 		) {
 183 | 			return await opts.storeClientSecret.encrypt(clientSecret);
 184 | 		}
 185 | 
 186 | 		return clientSecret;
 187 | 	}
 188 | 
 189 | 	/**
 190 | 	 * Verify stored client secret against provided client secret
 191 | 	 */
 192 | 	async function verifyStoredClientSecret(
 193 | 		ctx: GenericEndpointContext,
 194 | 		storedClientSecret: string,
 195 | 		clientSecret: string,
 196 | 	): Promise<boolean> {
 197 | 		if (opts.storeClientSecret === "encrypted") {
 198 | 			return (
 199 | 				(await symmetricDecrypt({
 200 | 					key: ctx.context.secret,
 201 | 					data: storedClientSecret,
 202 | 				})) === clientSecret
 203 | 			);
 204 | 		}
 205 | 		if (opts.storeClientSecret === "hashed") {
 206 | 			const hashedClientSecret = await defaultClientSecretHasher(clientSecret);
 207 | 			return hashedClientSecret === storedClientSecret;
 208 | 		}
 209 | 		if (
 210 | 			typeof opts.storeClientSecret === "object" &&
 211 | 			"hash" in opts.storeClientSecret
 212 | 		) {
 213 | 			const hashedClientSecret =
 214 | 				await opts.storeClientSecret.hash(clientSecret);
 215 | 			return hashedClientSecret === storedClientSecret;
 216 | 		}
 217 | 		if (
 218 | 			typeof opts.storeClientSecret === "object" &&
 219 | 			"decrypt" in opts.storeClientSecret
 220 | 		) {
 221 | 			const decryptedClientSecret =
 222 | 				await opts.storeClientSecret.decrypt(storedClientSecret);
 223 | 			return decryptedClientSecret === clientSecret;
 224 | 		}
 225 | 
 226 | 		return clientSecret === storedClientSecret;
 227 | 	}
 228 | 
 229 | 	return {
 230 | 		id: "oidc",
 231 | 		hooks: {
 232 | 			after: [
 233 | 				{
 234 | 					matcher() {
 235 | 						return true;
 236 | 					},
 237 | 					handler: createAuthMiddleware(async (ctx) => {
 238 | 						const cookie = await ctx.getSignedCookie(
 239 | 							"oidc_login_prompt",
 240 | 							ctx.context.secret,
 241 | 						);
 242 | 						const cookieName = ctx.context.authCookies.sessionToken.name;
 243 | 						const parsedSetCookieHeader = parseSetCookieHeader(
 244 | 							ctx.context.responseHeaders?.get("set-cookie") || "",
 245 | 						);
 246 | 						const hasSessionToken = parsedSetCookieHeader.has(cookieName);
 247 | 						if (!cookie || !hasSessionToken) {
 248 | 							return;
 249 | 						}
 250 | 						ctx.setCookie("oidc_login_prompt", "", {
 251 | 							maxAge: 0,
 252 | 						});
 253 | 						const sessionCookie = parsedSetCookieHeader.get(cookieName)?.value;
 254 | 						const sessionToken = sessionCookie?.split(".")[0]!;
 255 | 						if (!sessionToken) {
 256 | 							return;
 257 | 						}
 258 | 						const session =
 259 | 							await ctx.context.internalAdapter.findSession(sessionToken);
 260 | 						if (!session) {
 261 | 							return;
 262 | 						}
 263 | 						ctx.query = JSON.parse(cookie);
 264 | 						// Don't force prompt to "consent" - let the authorize function
 265 | 						// determine if consent is needed based on OIDC spec requirements
 266 | 						ctx.context.session = session;
 267 | 						const response = await authorize(ctx, opts);
 268 | 						return response;
 269 | 					}),
 270 | 				},
 271 | 			],
 272 | 		},
 273 | 		endpoints: {
 274 | 			getOpenIdConfig: createAuthEndpoint(
 275 | 				"/.well-known/openid-configuration",
 276 | 				{
 277 | 					method: "GET",
 278 | 					metadata: {
 279 | 						isAction: false,
 280 | 					},
 281 | 				},
 282 | 				async (ctx) => {
 283 | 					const metadata = getMetadata(ctx, options);
 284 | 					return ctx.json(metadata);
 285 | 				},
 286 | 			),
 287 | 			oAuth2authorize: createAuthEndpoint(
 288 | 				"/oauth2/authorize",
 289 | 				{
 290 | 					method: "GET",
 291 | 					query: z.record(z.string(), z.any()),
 292 | 					metadata: {
 293 | 						openapi: {
 294 | 							description: "Authorize an OAuth2 request",
 295 | 							responses: {
 296 | 								"200": {
 297 | 									description: "Authorization response generated successfully",
 298 | 									content: {
 299 | 										"application/json": {
 300 | 											schema: {
 301 | 												type: "object",
 302 | 												additionalProperties: true,
 303 | 												description:
 304 | 													"Authorization response, contents depend on the authorize function implementation",
 305 | 											},
 306 | 										},
 307 | 									},
 308 | 								},
 309 | 							},
 310 | 						},
 311 | 					},
 312 | 				},
 313 | 				async (ctx) => {
 314 | 					return authorize(ctx, opts);
 315 | 				},
 316 | 			),
 317 | 			oAuthConsent: createAuthEndpoint(
 318 | 				"/oauth2/consent",
 319 | 				{
 320 | 					method: "POST",
 321 | 					body: z.object({
 322 | 						accept: z.boolean(),
 323 | 						consent_code: z.string().optional().nullish(),
 324 | 					}),
 325 | 					use: [sessionMiddleware],
 326 | 					metadata: {
 327 | 						openapi: {
 328 | 							description:
 329 | 								"Handle OAuth2 consent. Supports both URL parameter-based flows (consent_code in body) and cookie-based flows (signed cookie).",
 330 | 							requestBody: {
 331 | 								required: true,
 332 | 								content: {
 333 | 									"application/json": {
 334 | 										schema: {
 335 | 											type: "object",
 336 | 											properties: {
 337 | 												accept: {
 338 | 													type: "boolean",
 339 | 													description:
 340 | 														"Whether the user accepts or denies the consent request",
 341 | 												},
 342 | 												consent_code: {
 343 | 													type: "string",
 344 | 													description:
 345 | 														"The consent code from the authorization request. Optional if using cookie-based flow.",
 346 | 												},
 347 | 											},
 348 | 											required: ["accept"],
 349 | 										},
 350 | 									},
 351 | 								},
 352 | 							},
 353 | 							responses: {
 354 | 								"200": {
 355 | 									description: "Consent processed successfully",
 356 | 									content: {
 357 | 										"application/json": {
 358 | 											schema: {
 359 | 												type: "object",
 360 | 												properties: {
 361 | 													redirectURI: {
 362 | 														type: "string",
 363 | 														format: "uri",
 364 | 														description:
 365 | 															"The URI to redirect to, either with an authorization code or an error",
 366 | 													},
 367 | 												},
 368 | 												required: ["redirectURI"],
 369 | 											},
 370 | 										},
 371 | 									},
 372 | 								},
 373 | 							},
 374 | 						},
 375 | 					},
 376 | 				},
 377 | 				async (ctx) => {
 378 | 					// Support both consent flow methods:
 379 | 					// 1. URL parameter-based: consent_code in request body (standard OAuth2 pattern)
 380 | 					// 2. Cookie-based: using signed cookie for stateful consent flows
 381 | 					let consentCode: string | null = ctx.body.consent_code || null;
 382 | 
 383 | 					if (!consentCode) {
 384 | 						// Check for cookie-based consent flow
 385 | 						consentCode = await ctx.getSignedCookie(
 386 | 							"oidc_consent_prompt",
 387 | 							ctx.context.secret,
 388 | 						);
 389 | 					}
 390 | 
 391 | 					if (!consentCode) {
 392 | 						throw new APIError("UNAUTHORIZED", {
 393 | 							error_description:
 394 | 								"consent_code is required (either in body or cookie)",
 395 | 							error: "invalid_request",
 396 | 						});
 397 | 					}
 398 | 
 399 | 					const verification =
 400 | 						await ctx.context.internalAdapter.findVerificationValue(
 401 | 							consentCode,
 402 | 						);
 403 | 					if (!verification) {
 404 | 						throw new APIError("UNAUTHORIZED", {
 405 | 							error_description: "Invalid code",
 406 | 							error: "invalid_request",
 407 | 						});
 408 | 					}
 409 | 					if (verification.expiresAt < new Date()) {
 410 | 						throw new APIError("UNAUTHORIZED", {
 411 | 							error_description: "Code expired",
 412 | 							error: "invalid_request",
 413 | 						});
 414 | 					}
 415 | 
 416 | 					// Clear the cookie
 417 | 					ctx.setCookie("oidc_consent_prompt", "", {
 418 | 						maxAge: 0,
 419 | 					});
 420 | 
 421 | 					const value = JSON.parse(verification.value) as CodeVerificationValue;
 422 | 					if (!value.requireConsent) {
 423 | 						throw new APIError("UNAUTHORIZED", {
 424 | 							error_description: "Consent not required",
 425 | 							error: "invalid_request",
 426 | 						});
 427 | 					}
 428 | 
 429 | 					if (!ctx.body.accept) {
 430 | 						await ctx.context.internalAdapter.deleteVerificationValue(
 431 | 							verification.id,
 432 | 						);
 433 | 						return ctx.json({
 434 | 							redirectURI: `${value.redirectURI}?error=access_denied&error_description=User denied access`,
 435 | 						});
 436 | 					}
 437 | 					const code = generateRandomString(32, "a-z", "A-Z", "0-9");
 438 | 					const codeExpiresInMs = opts.codeExpiresIn * 1000;
 439 | 					const expiresAt = new Date(Date.now() + codeExpiresInMs);
 440 | 					await ctx.context.internalAdapter.updateVerificationValue(
 441 | 						verification.id,
 442 | 						{
 443 | 							value: JSON.stringify({
 444 | 								...value,
 445 | 								requireConsent: false,
 446 | 							}),
 447 | 							identifier: code,
 448 | 							expiresAt,
 449 | 						},
 450 | 					);
 451 | 					await ctx.context.adapter.create({
 452 | 						model: modelName.oauthConsent,
 453 | 						data: {
 454 | 							clientId: value.clientId,
 455 | 							userId: value.userId,
 456 | 							scopes: value.scope.join(" "),
 457 | 							consentGiven: true,
 458 | 							createdAt: new Date(),
 459 | 							updatedAt: new Date(),
 460 | 						},
 461 | 					});
 462 | 					const redirectURI = new URL(value.redirectURI);
 463 | 					redirectURI.searchParams.set("code", code);
 464 | 					if (value.state) redirectURI.searchParams.set("state", value.state);
 465 | 					return ctx.json({
 466 | 						redirectURI: redirectURI.toString(),
 467 | 					});
 468 | 				},
 469 | 			),
 470 | 			oAuth2token: createAuthEndpoint(
 471 | 				"/oauth2/token",
 472 | 				{
 473 | 					method: "POST",
 474 | 					body: z.record(z.any(), z.any()),
 475 | 					metadata: {
 476 | 						isAction: false,
 477 | 					},
 478 | 				},
 479 | 				async (ctx) => {
 480 | 					let { body } = ctx;
 481 | 					if (!body) {
 482 | 						throw new APIError("BAD_REQUEST", {
 483 | 							error_description: "request body not found",
 484 | 							error: "invalid_request",
 485 | 						});
 486 | 					}
 487 | 					if (body instanceof FormData) {
 488 | 						body = Object.fromEntries(body.entries());
 489 | 					}
 490 | 					if (!(body instanceof Object)) {
 491 | 						throw new APIError("BAD_REQUEST", {
 492 | 							error_description: "request body is not an object",
 493 | 							error: "invalid_request",
 494 | 						});
 495 | 					}
 496 | 					let { client_id, client_secret } = body;
 497 | 					const authorization =
 498 | 						ctx.request?.headers.get("authorization") || null;
 499 | 					if (
 500 | 						authorization &&
 501 | 						!client_id &&
 502 | 						!client_secret &&
 503 | 						authorization.startsWith("Basic ")
 504 | 					) {
 505 | 						try {
 506 | 							const encoded = authorization.replace("Basic ", "");
 507 | 							const decoded = new TextDecoder().decode(base64.decode(encoded));
 508 | 							if (!decoded.includes(":")) {
 509 | 								throw new APIError("UNAUTHORIZED", {
 510 | 									error_description: "invalid authorization header format",
 511 | 									error: "invalid_client",
 512 | 								});
 513 | 							}
 514 | 							const [id, secret] = decoded.split(":");
 515 | 							if (!id || !secret) {
 516 | 								throw new APIError("UNAUTHORIZED", {
 517 | 									error_description: "invalid authorization header format",
 518 | 									error: "invalid_client",
 519 | 								});
 520 | 							}
 521 | 							client_id = id;
 522 | 							client_secret = secret;
 523 | 						} catch (error) {
 524 | 							throw new APIError("UNAUTHORIZED", {
 525 | 								error_description: "invalid authorization header format",
 526 | 								error: "invalid_client",
 527 | 							});
 528 | 						}
 529 | 					}
 530 | 
 531 | 					const now = Date.now();
 532 | 					const iat = Math.floor(now / 1000);
 533 | 					const exp = iat + (opts.accessTokenExpiresIn ?? 3600);
 534 | 
 535 | 					const accessTokenExpiresAt = new Date(exp * 1000);
 536 | 					const refreshTokenExpiresAt = new Date(
 537 | 						(iat + (opts.refreshTokenExpiresIn ?? 604800)) * 1000,
 538 | 					);
 539 | 
 540 | 					const {
 541 | 						grant_type,
 542 | 						code,
 543 | 						redirect_uri,
 544 | 						refresh_token,
 545 | 						code_verifier,
 546 | 					} = body;
 547 | 					if (grant_type === "refresh_token") {
 548 | 						if (!refresh_token) {
 549 | 							throw new APIError("BAD_REQUEST", {
 550 | 								error_description: "refresh_token is required",
 551 | 								error: "invalid_request",
 552 | 							});
 553 | 						}
 554 | 						const token = await ctx.context.adapter.findOne<OAuthAccessToken>({
 555 | 							model: modelName.oauthAccessToken,
 556 | 							where: [
 557 | 								{
 558 | 									field: "refreshToken",
 559 | 									value: refresh_token.toString(),
 560 | 								},
 561 | 							],
 562 | 						});
 563 | 						if (!token) {
 564 | 							throw new APIError("UNAUTHORIZED", {
 565 | 								error_description: "invalid refresh token",
 566 | 								error: "invalid_grant",
 567 | 							});
 568 | 						}
 569 | 						if (token.clientId !== client_id?.toString()) {
 570 | 							throw new APIError("UNAUTHORIZED", {
 571 | 								error_description: "invalid client_id",
 572 | 								error: "invalid_client",
 573 | 							});
 574 | 						}
 575 | 						if (token.refreshTokenExpiresAt < new Date()) {
 576 | 							throw new APIError("UNAUTHORIZED", {
 577 | 								error_description: "refresh token expired",
 578 | 								error: "invalid_grant",
 579 | 							});
 580 | 						}
 581 | 						const accessToken = generateRandomString(32, "a-z", "A-Z");
 582 | 						const newRefreshToken = generateRandomString(32, "a-z", "A-Z");
 583 | 
 584 | 						await ctx.context.adapter.create({
 585 | 							model: modelName.oauthAccessToken,
 586 | 							data: {
 587 | 								accessToken,
 588 | 								refreshToken: newRefreshToken,
 589 | 								accessTokenExpiresAt,
 590 | 								refreshTokenExpiresAt,
 591 | 								clientId: client_id.toString(),
 592 | 								userId: token.userId,
 593 | 								scopes: token.scopes,
 594 | 								createdAt: new Date(iat * 1000),
 595 | 								updatedAt: new Date(iat * 1000),
 596 | 							},
 597 | 						});
 598 | 						return ctx.json({
 599 | 							access_token: accessToken,
 600 | 							token_type: "Bearer",
 601 | 							expires_in: opts.accessTokenExpiresIn,
 602 | 							refresh_token: newRefreshToken,
 603 | 							scope: token.scopes,
 604 | 						});
 605 | 					}
 606 | 
 607 | 					if (!code) {
 608 | 						throw new APIError("BAD_REQUEST", {
 609 | 							error_description: "code is required",
 610 | 							error: "invalid_request",
 611 | 						});
 612 | 					}
 613 | 
 614 | 					if (options.requirePKCE && !code_verifier) {
 615 | 						throw new APIError("BAD_REQUEST", {
 616 | 							error_description: "code verifier is missing",
 617 | 							error: "invalid_request",
 618 | 						});
 619 | 					}
 620 | 
 621 | 					/**
 622 | 					 * We need to check if the code is valid before we can proceed
 623 | 					 * with the rest of the request.
 624 | 					 */
 625 | 					const verificationValue =
 626 | 						await ctx.context.internalAdapter.findVerificationValue(
 627 | 							code.toString(),
 628 | 						);
 629 | 					if (!verificationValue) {
 630 | 						throw new APIError("UNAUTHORIZED", {
 631 | 							error_description: "invalid code",
 632 | 							error: "invalid_grant",
 633 | 						});
 634 | 					}
 635 | 					if (verificationValue.expiresAt < new Date()) {
 636 | 						throw new APIError("UNAUTHORIZED", {
 637 | 							error_description: "code expired",
 638 | 							error: "invalid_grant",
 639 | 						});
 640 | 					}
 641 | 
 642 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 643 | 						verificationValue.id,
 644 | 					);
 645 | 					if (!client_id) {
 646 | 						throw new APIError("UNAUTHORIZED", {
 647 | 							error_description: "client_id is required",
 648 | 							error: "invalid_client",
 649 | 						});
 650 | 					}
 651 | 					if (!grant_type) {
 652 | 						throw new APIError("BAD_REQUEST", {
 653 | 							error_description: "grant_type is required",
 654 | 							error: "invalid_request",
 655 | 						});
 656 | 					}
 657 | 					if (grant_type !== "authorization_code") {
 658 | 						throw new APIError("BAD_REQUEST", {
 659 | 							error_description: "grant_type must be 'authorization_code'",
 660 | 							error: "unsupported_grant_type",
 661 | 						});
 662 | 					}
 663 | 
 664 | 					if (!redirect_uri) {
 665 | 						throw new APIError("BAD_REQUEST", {
 666 | 							error_description: "redirect_uri is required",
 667 | 							error: "invalid_request",
 668 | 						});
 669 | 					}
 670 | 
 671 | 					const client = await getClient(
 672 | 						client_id.toString(),
 673 | 						ctx.context.adapter,
 674 | 						trustedClients,
 675 | 					);
 676 | 					if (!client) {
 677 | 						throw new APIError("UNAUTHORIZED", {
 678 | 							error_description: "invalid client_id",
 679 | 							error: "invalid_client",
 680 | 						});
 681 | 					}
 682 | 					if (client.disabled) {
 683 | 						throw new APIError("UNAUTHORIZED", {
 684 | 							error_description: "client is disabled",
 685 | 							error: "invalid_client",
 686 | 						});
 687 | 					}
 688 | 
 689 | 					const value = JSON.parse(
 690 | 						verificationValue.value,
 691 | 					) as CodeVerificationValue;
 692 | 					if (value.clientId !== client_id.toString()) {
 693 | 						throw new APIError("UNAUTHORIZED", {
 694 | 							error_description: "invalid client_id",
 695 | 							error: "invalid_client",
 696 | 						});
 697 | 					}
 698 | 					if (value.redirectURI !== redirect_uri.toString()) {
 699 | 						throw new APIError("UNAUTHORIZED", {
 700 | 							error_description: "invalid redirect_uri",
 701 | 							error: "invalid_client",
 702 | 						});
 703 | 					}
 704 | 					if (value.codeChallenge && !code_verifier) {
 705 | 						throw new APIError("BAD_REQUEST", {
 706 | 							error_description: "code verifier is missing",
 707 | 							error: "invalid_request",
 708 | 						});
 709 | 					}
 710 | 					if (client.type === "public") {
 711 | 						// For public clients (type: 'public'), validate PKCE instead of client_secret
 712 | 						if (!code_verifier) {
 713 | 							throw new APIError("BAD_REQUEST", {
 714 | 								error_description:
 715 | 									"code verifier is required for public clients",
 716 | 								error: "invalid_request",
 717 | 							});
 718 | 						}
 719 | 						// PKCE validation happens later in the flow, so we skip client_secret validation
 720 | 					} else {
 721 | 						if (!client.clientSecret || !client_secret) {
 722 | 							throw new APIError("UNAUTHORIZED", {
 723 | 								error_description:
 724 | 									"client_secret is required for confidential clients",
 725 | 								error: "invalid_client",
 726 | 							});
 727 | 						}
 728 | 						const isValidSecret = await verifyStoredClientSecret(
 729 | 							ctx,
 730 | 							client.clientSecret,
 731 | 							client_secret.toString(),
 732 | 						);
 733 | 						if (!isValidSecret) {
 734 | 							throw new APIError("UNAUTHORIZED", {
 735 | 								error_description: "invalid client_secret",
 736 | 								error: "invalid_client",
 737 | 							});
 738 | 						}
 739 | 					}
 740 | 					const challenge =
 741 | 						value.codeChallengeMethod === "plain"
 742 | 							? code_verifier
 743 | 							: await createHash("SHA-256", "base64urlnopad").digest(
 744 | 									code_verifier,
 745 | 								);
 746 | 
 747 | 					if (challenge !== value.codeChallenge) {
 748 | 						throw new APIError("UNAUTHORIZED", {
 749 | 							error_description: "code verification failed",
 750 | 							error: "invalid_request",
 751 | 						});
 752 | 					}
 753 | 
 754 | 					const requestedScopes = value.scope;
 755 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 756 | 						verificationValue.id,
 757 | 					);
 758 | 					const accessToken = generateRandomString(32, "a-z", "A-Z");
 759 | 					const refreshToken = generateRandomString(32, "A-Z", "a-z");
 760 | 					await ctx.context.adapter.create({
 761 | 						model: modelName.oauthAccessToken,
 762 | 						data: {
 763 | 							accessToken,
 764 | 							refreshToken,
 765 | 							accessTokenExpiresAt,
 766 | 							refreshTokenExpiresAt,
 767 | 							clientId: client_id.toString(),
 768 | 							userId: value.userId,
 769 | 							scopes: requestedScopes.join(" "),
 770 | 							createdAt: new Date(iat * 1000),
 771 | 							updatedAt: new Date(iat * 1000),
 772 | 						},
 773 | 					});
 774 | 					const user = await ctx.context.internalAdapter.findUserById(
 775 | 						value.userId,
 776 | 					);
 777 | 					if (!user) {
 778 | 						throw new APIError("UNAUTHORIZED", {
 779 | 							error_description: "user not found",
 780 | 							error: "invalid_grant",
 781 | 						});
 782 | 					}
 783 | 
 784 | 					const profile = {
 785 | 						given_name: user.name.split(" ")[0]!,
 786 | 						family_name: user.name.split(" ")[1]!,
 787 | 						name: user.name,
 788 | 						profile: user.image,
 789 | 						updated_at: new Date(user.updatedAt).toISOString(),
 790 | 					};
 791 | 					const email = {
 792 | 						email: user.email,
 793 | 						email_verified: user.emailVerified,
 794 | 					};
 795 | 					const userClaims = {
 796 | 						...(requestedScopes.includes("profile") ? profile : {}),
 797 | 						...(requestedScopes.includes("email") ? email : {}),
 798 | 					};
 799 | 
 800 | 					const additionalUserClaims = options.getAdditionalUserInfoClaim
 801 | 						? await options.getAdditionalUserInfoClaim(
 802 | 								user,
 803 | 								requestedScopes,
 804 | 								client,
 805 | 							)
 806 | 						: {};
 807 | 
 808 | 					const payload = {
 809 | 						sub: user.id,
 810 | 						aud: client_id.toString(),
 811 | 						iat: Date.now(),
 812 | 						auth_time: ctx.context.session
 813 | 							? new Date(ctx.context.session.session.createdAt).getTime()
 814 | 							: undefined,
 815 | 						nonce: value.nonce,
 816 | 						acr: "urn:mace:incommon:iap:silver", // default to silver - ⚠︎ this should be configurable and should be validated against the client's metadata
 817 | 						...userClaims,
 818 | 						...additionalUserClaims,
 819 | 					};
 820 | 					const expirationTime =
 821 | 						Math.floor(Date.now() / 1000) + opts.accessTokenExpiresIn;
 822 | 
 823 | 					let idToken: string;
 824 | 
 825 | 					// The JWT plugin is enabled, so we use the JWKS keys to sign
 826 | 					if (options.useJWTPlugin) {
 827 | 						const jwtPlugin = getJwtPlugin(ctx);
 828 | 						if (!jwtPlugin) {
 829 | 							ctx.context.logger.error(
 830 | 								"OIDC: `useJWTPlugin` is enabled but the JWT plugin is not available. Make sure you have the JWT Plugin in your plugins array or set `useJWTPlugin` to false.",
 831 | 							);
 832 | 							throw new APIError("INTERNAL_SERVER_ERROR", {
 833 | 								error_description: "JWT plugin is not enabled",
 834 | 								error: "internal_server_error",
 835 | 							});
 836 | 						}
 837 | 						idToken = await getJwtToken(
 838 | 							{
 839 | 								...ctx,
 840 | 								context: {
 841 | 									...ctx.context,
 842 | 									session: {
 843 | 										session: {
 844 | 											id: generateRandomString(32, "a-z", "A-Z"),
 845 | 											createdAt: new Date(iat * 1000),
 846 | 											updatedAt: new Date(iat * 1000),
 847 | 											userId: user.id,
 848 | 											expiresAt: accessTokenExpiresAt,
 849 | 											token: accessToken,
 850 | 											ipAddress: ctx.request?.headers.get("x-forwarded-for"),
 851 | 										},
 852 | 										user,
 853 | 									},
 854 | 								},
 855 | 							},
 856 | 							{
 857 | 								...jwtPlugin.options,
 858 | 								jwt: {
 859 | 									...jwtPlugin.options?.jwt,
 860 | 									getSubject: () => user.id,
 861 | 									audience: client_id.toString(),
 862 | 									issuer: ctx.context.options.baseURL,
 863 | 									expirationTime,
 864 | 									definePayload: () => payload,
 865 | 								},
 866 | 							},
 867 | 						);
 868 | 
 869 | 						// If the JWT token is not enabled, create a key and use it to sign
 870 | 					} else {
 871 | 						idToken = await new SignJWT(payload)
 872 | 							.setProtectedHeader({ alg: "HS256" })
 873 | 							.setIssuedAt(iat)
 874 | 							.setExpirationTime(accessTokenExpiresAt)
 875 | 							.sign(new TextEncoder().encode(client.clientSecret));
 876 | 					}
 877 | 
 878 | 					return ctx.json(
 879 | 						{
 880 | 							access_token: accessToken,
 881 | 							token_type: "Bearer",
 882 | 							expires_in: opts.accessTokenExpiresIn,
 883 | 							refresh_token: requestedScopes.includes("offline_access")
 884 | 								? refreshToken
 885 | 								: undefined,
 886 | 							scope: requestedScopes.join(" "),
 887 | 							id_token: requestedScopes.includes("openid")
 888 | 								? idToken
 889 | 								: undefined,
 890 | 						},
 891 | 						{
 892 | 							headers: {
 893 | 								"Cache-Control": "no-store",
 894 | 								Pragma: "no-cache",
 895 | 							},
 896 | 						},
 897 | 					);
 898 | 				},
 899 | 			),
 900 | 			oAuth2userInfo: createAuthEndpoint(
 901 | 				"/oauth2/userinfo",
 902 | 				{
 903 | 					method: "GET",
 904 | 
 905 | 					metadata: {
 906 | 						isAction: false,
 907 | 						openapi: {
 908 | 							description: "Get OAuth2 user information",
 909 | 							responses: {
 910 | 								"200": {
 911 | 									description: "User information retrieved successfully",
 912 | 									content: {
 913 | 										"application/json": {
 914 | 											schema: {
 915 | 												type: "object",
 916 | 												properties: {
 917 | 													sub: {
 918 | 														type: "string",
 919 | 														description: "Subject identifier (user ID)",
 920 | 													},
 921 | 													email: {
 922 | 														type: "string",
 923 | 														format: "email",
 924 | 														nullable: true,
 925 | 														description:
 926 | 															"User's email address, included if 'email' scope is granted",
 927 | 													},
 928 | 													name: {
 929 | 														type: "string",
 930 | 														nullable: true,
 931 | 														description:
 932 | 															"User's full name, included if 'profile' scope is granted",
 933 | 													},
 934 | 													picture: {
 935 | 														type: "string",
 936 | 														format: "uri",
 937 | 														nullable: true,
 938 | 														description:
 939 | 															"User's profile picture URL, included if 'profile' scope is granted",
 940 | 													},
 941 | 													given_name: {
 942 | 														type: "string",
 943 | 														nullable: true,
 944 | 														description:
 945 | 															"User's given name, included if 'profile' scope is granted",
 946 | 													},
 947 | 													family_name: {
 948 | 														type: "string",
 949 | 														nullable: true,
 950 | 														description:
 951 | 															"User's family name, included if 'profile' scope is granted",
 952 | 													},
 953 | 													email_verified: {
 954 | 														type: "boolean",
 955 | 														nullable: true,
 956 | 														description:
 957 | 															"Whether the email is verified, included if 'email' scope is granted",
 958 | 													},
 959 | 												},
 960 | 												required: ["sub"],
 961 | 											},
 962 | 										},
 963 | 									},
 964 | 								},
 965 | 							},
 966 | 						},
 967 | 					},
 968 | 				},
 969 | 				async (ctx) => {
 970 | 					if (!ctx.request) {
 971 | 						throw new APIError("UNAUTHORIZED", {
 972 | 							error_description: "request not found",
 973 | 							error: "invalid_request",
 974 | 						});
 975 | 					}
 976 | 					const authorization = ctx.request.headers.get("authorization");
 977 | 					if (!authorization) {
 978 | 						throw new APIError("UNAUTHORIZED", {
 979 | 							error_description: "authorization header not found",
 980 | 							error: "invalid_request",
 981 | 						});
 982 | 					}
 983 | 					const token = authorization.replace("Bearer ", "");
 984 | 					const accessToken =
 985 | 						await ctx.context.adapter.findOne<OAuthAccessToken>({
 986 | 							model: modelName.oauthAccessToken,
 987 | 							where: [
 988 | 								{
 989 | 									field: "accessToken",
 990 | 									value: token,
 991 | 								},
 992 | 							],
 993 | 						});
 994 | 					if (!accessToken) {
 995 | 						throw new APIError("UNAUTHORIZED", {
 996 | 							error_description: "invalid access token",
 997 | 							error: "invalid_token",
 998 | 						});
 999 | 					}
1000 | 					if (accessToken.accessTokenExpiresAt < new Date()) {
1001 | 						throw new APIError("UNAUTHORIZED", {
1002 | 							error_description: "The Access Token expired",
1003 | 							error: "invalid_token",
1004 | 						});
1005 | 					}
1006 | 
1007 | 					const client = await getClient(
1008 | 						accessToken.clientId,
1009 | 						ctx.context.adapter,
1010 | 						trustedClients,
1011 | 					);
1012 | 					if (!client) {
1013 | 						throw new APIError("UNAUTHORIZED", {
1014 | 							error_description: "client not found",
1015 | 							error: "invalid_token",
1016 | 						});
1017 | 					}
1018 | 
1019 | 					const user = await ctx.context.internalAdapter.findUserById(
1020 | 						accessToken.userId,
1021 | 					);
1022 | 					if (!user) {
1023 | 						throw new APIError("UNAUTHORIZED", {
1024 | 							error_description: "user not found",
1025 | 							error: "invalid_token",
1026 | 						});
1027 | 					}
1028 | 					const requestedScopes = accessToken.scopes.split(" ");
1029 | 					const baseUserClaims = {
1030 | 						sub: user.id,
1031 | 						email: requestedScopes.includes("email") ? user.email : undefined,
1032 | 						name: requestedScopes.includes("profile") ? user.name : undefined,
1033 | 						picture: requestedScopes.includes("profile")
1034 | 							? user.image
1035 | 							: undefined,
1036 | 						given_name: requestedScopes.includes("profile")
1037 | 							? user.name.split(" ")[0]!
1038 | 							: undefined,
1039 | 						family_name: requestedScopes.includes("profile")
1040 | 							? user.name.split(" ")[1]!
1041 | 							: undefined,
1042 | 						email_verified: requestedScopes.includes("email")
1043 | 							? user.emailVerified
1044 | 							: undefined,
1045 | 					};
1046 | 					const userClaims = options.getAdditionalUserInfoClaim
1047 | 						? await options.getAdditionalUserInfoClaim(
1048 | 								user,
1049 | 								requestedScopes,
1050 | 								client,
1051 | 							)
1052 | 						: baseUserClaims;
1053 | 					return ctx.json({
1054 | 						...baseUserClaims,
1055 | 						...userClaims,
1056 | 					});
1057 | 				},
1058 | 			),
1059 | 			/**
1060 | 			 * ### Endpoint
1061 | 			 *
1062 | 			 * POST `/oauth2/register`
1063 | 			 *
1064 | 			 * ### API Methods
1065 | 			 *
1066 | 			 * **server:**
1067 | 			 * `auth.api.registerOAuthApplication`
1068 | 			 *
1069 | 			 * **client:**
1070 | 			 * `authClient.oauth2.register`
1071 | 			 *
1072 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/oidc-provider#api-method-oauth2-register)
1073 | 			 */
1074 | 			registerOAuthApplication: createAuthEndpoint(
1075 | 				"/oauth2/register",
1076 | 				{
1077 | 					method: "POST",
1078 | 					body: z.object({
1079 | 						redirect_uris: z.array(z.string()).meta({
1080 | 							description:
1081 | 								'A list of redirect URIs. Eg: ["https://client.example.com/callback"]',
1082 | 						}),
1083 | 						token_endpoint_auth_method: z
1084 | 							.enum(["none", "client_secret_basic", "client_secret_post"])
1085 | 							.meta({
1086 | 								description:
1087 | 									'The authentication method for the token endpoint. Eg: "client_secret_basic"',
1088 | 							})
1089 | 							.default("client_secret_basic")
1090 | 							.optional(),
1091 | 						grant_types: z
1092 | 							.array(
1093 | 								z.enum([
1094 | 									"authorization_code",
1095 | 									"implicit",
1096 | 									"password",
1097 | 									"client_credentials",
1098 | 									"refresh_token",
1099 | 									"urn:ietf:params:oauth:grant-type:jwt-bearer",
1100 | 									"urn:ietf:params:oauth:grant-type:saml2-bearer",
1101 | 								]),
1102 | 							)
1103 | 							.meta({
1104 | 								description:
1105 | 									'The grant types supported by the application. Eg: ["authorization_code"]',
1106 | 							})
1107 | 							.default(["authorization_code"])
1108 | 							.optional(),
1109 | 						response_types: z
1110 | 							.array(z.enum(["code", "token"]))
1111 | 							.meta({
1112 | 								description:
1113 | 									'The response types supported by the application. Eg: ["code"]',
1114 | 							})
1115 | 							.default(["code"])
1116 | 							.optional(),
1117 | 						client_name: z
1118 | 							.string()
1119 | 							.meta({
1120 | 								description: 'The name of the application. Eg: "My App"',
1121 | 							})
1122 | 							.optional(),
1123 | 						client_uri: z
1124 | 							.string()
1125 | 							.meta({
1126 | 								description:
1127 | 									'The URI of the application. Eg: "https://client.example.com"',
1128 | 							})
1129 | 							.optional(),
1130 | 						logo_uri: z
1131 | 							.string()
1132 | 							.meta({
1133 | 								description:
1134 | 									'The URI of the application logo. Eg: "https://client.example.com/logo.png"',
1135 | 							})
1136 | 							.optional(),
1137 | 						scope: z
1138 | 							.string()
1139 | 							.meta({
1140 | 								description:
1141 | 									'The scopes supported by the application. Separated by spaces. Eg: "profile email"',
1142 | 							})
1143 | 							.optional(),
1144 | 						contacts: z
1145 | 							.array(z.string())
1146 | 							.meta({
1147 | 								description:
1148 | 									'The contact information for the application. Eg: ["[email protected]"]',
1149 | 							})
1150 | 							.optional(),
1151 | 						tos_uri: z
1152 | 							.string()
1153 | 							.meta({
1154 | 								description:
1155 | 									'The URI of the application terms of service. Eg: "https://client.example.com/tos"',
1156 | 							})
1157 | 							.optional(),
1158 | 						policy_uri: z
1159 | 							.string()
1160 | 							.meta({
1161 | 								description:
1162 | 									'The URI of the application privacy policy. Eg: "https://client.example.com/policy"',
1163 | 							})
1164 | 							.optional(),
1165 | 						jwks_uri: z
1166 | 							.string()
1167 | 							.meta({
1168 | 								description:
1169 | 									'The URI of the application JWKS. Eg: "https://client.example.com/jwks"',
1170 | 							})
1171 | 							.optional(),
1172 | 						jwks: z
1173 | 							.record(z.any(), z.any())
1174 | 							.meta({
1175 | 								description:
1176 | 									'The JWKS of the application. Eg: {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]}',
1177 | 							})
1178 | 							.optional(),
1179 | 						metadata: z
1180 | 							.record(z.any(), z.any())
1181 | 							.meta({
1182 | 								description:
1183 | 									'The metadata of the application. Eg: {"key": "value"}',
1184 | 							})
1185 | 							.optional(),
1186 | 						software_id: z
1187 | 							.string()
1188 | 							.meta({
1189 | 								description:
1190 | 									'The software ID of the application. Eg: "my-software"',
1191 | 							})
1192 | 							.optional(),
1193 | 						software_version: z
1194 | 							.string()
1195 | 							.meta({
1196 | 								description:
1197 | 									'The software version of the application. Eg: "1.0.0"',
1198 | 							})
1199 | 							.optional(),
1200 | 						software_statement: z
1201 | 							.string()
1202 | 							.meta({
1203 | 								description: "The software statement of the application.",
1204 | 							})
1205 | 							.optional(),
1206 | 					}),
1207 | 					metadata: {
1208 | 						openapi: {
1209 | 							description: "Register an OAuth2 application",
1210 | 							responses: {
1211 | 								"200": {
1212 | 									description: "OAuth2 application registered successfully",
1213 | 									content: {
1214 | 										"application/json": {
1215 | 											schema: {
1216 | 												type: "object",
1217 | 												properties: {
1218 | 													name: {
1219 | 														type: "string",
1220 | 														description: "Name of the OAuth2 application",
1221 | 													},
1222 | 													icon: {
1223 | 														type: "string",
1224 | 														nullable: true,
1225 | 														description: "Icon URL for the application",
1226 | 													},
1227 | 													metadata: {
1228 | 														type: "object",
1229 | 														additionalProperties: true,
1230 | 														nullable: true,
1231 | 														description:
1232 | 															"Additional metadata for the application",
1233 | 													},
1234 | 													clientId: {
1235 | 														type: "string",
1236 | 														description: "Unique identifier for the client",
1237 | 													},
1238 | 													clientSecret: {
1239 | 														type: "string",
1240 | 														description: "Secret key for the client",
1241 | 													},
1242 | 													redirectURLs: {
1243 | 														type: "array",
1244 | 														items: { type: "string", format: "uri" },
1245 | 														description: "List of allowed redirect URLs",
1246 | 													},
1247 | 													type: {
1248 | 														type: "string",
1249 | 														description: "Type of the client",
1250 | 														enum: ["web"],
1251 | 													},
1252 | 													authenticationScheme: {
1253 | 														type: "string",
1254 | 														description:
1255 | 															"Authentication scheme used by the client",
1256 | 														enum: ["client_secret"],
1257 | 													},
1258 | 													disabled: {
1259 | 														type: "boolean",
1260 | 														description: "Whether the client is disabled",
1261 | 														enum: [false],
1262 | 													},
1263 | 													userId: {
1264 | 														type: "string",
1265 | 														nullable: true,
1266 | 														description:
1267 | 															"ID of the user who registered the client, null if registered anonymously",
1268 | 													},
1269 | 													createdAt: {
1270 | 														type: "string",
1271 | 														format: "date-time",
1272 | 														description: "Creation timestamp",
1273 | 													},
1274 | 													updatedAt: {
1275 | 														type: "string",
1276 | 														format: "date-time",
1277 | 														description: "Last update timestamp",
1278 | 													},
1279 | 												},
1280 | 												required: [
1281 | 													"name",
1282 | 													"clientId",
1283 | 													"clientSecret",
1284 | 													"redirectURLs",
1285 | 													"type",
1286 | 													"authenticationScheme",
1287 | 													"disabled",
1288 | 													"createdAt",
1289 | 													"updatedAt",
1290 | 												],
1291 | 											},
1292 | 										},
1293 | 									},
1294 | 								},
1295 | 							},
1296 | 						},
1297 | 					},
1298 | 				},
1299 | 				async (ctx) => {
1300 | 					const body = ctx.body;
1301 | 					const session = await getSessionFromCtx(ctx);
1302 | 
1303 | 					// Check authorization
1304 | 					if (!session && !options.allowDynamicClientRegistration) {
1305 | 						throw new APIError("UNAUTHORIZED", {
1306 | 							error: "invalid_token",
1307 | 							error_description:
1308 | 								"Authentication required for client registration",
1309 | 						});
1310 | 					}
1311 | 
1312 | 					// Validate redirect URIs for redirect-based flows
1313 | 					if (
1314 | 						(!body.grant_types ||
1315 | 							body.grant_types.includes("authorization_code") ||
1316 | 							body.grant_types.includes("implicit")) &&
1317 | 						(!body.redirect_uris || body.redirect_uris.length === 0)
1318 | 					) {
1319 | 						throw new APIError("BAD_REQUEST", {
1320 | 							error: "invalid_redirect_uri",
1321 | 							error_description:
1322 | 								"Redirect URIs are required for authorization_code and implicit grant types",
1323 | 						});
1324 | 					}
1325 | 
1326 | 					// Validate correlation between grant_types and response_types
1327 | 					if (body.grant_types && body.response_types) {
1328 | 						if (
1329 | 							body.grant_types.includes("authorization_code") &&
1330 | 							!body.response_types.includes("code")
1331 | 						) {
1332 | 							throw new APIError("BAD_REQUEST", {
1333 | 								error: "invalid_client_metadata",
1334 | 								error_description:
1335 | 									"When 'authorization_code' grant type is used, 'code' response type must be included",
1336 | 							});
1337 | 						}
1338 | 						if (
1339 | 							body.grant_types.includes("implicit") &&
1340 | 							!body.response_types.includes("token")
1341 | 						) {
1342 | 							throw new APIError("BAD_REQUEST", {
1343 | 								error: "invalid_client_metadata",
1344 | 								error_description:
1345 | 									"When 'implicit' grant type is used, 'token' response type must be included",
1346 | 							});
1347 | 						}
1348 | 					}
1349 | 
1350 | 					const clientId =
1351 | 						options.generateClientId?.() ||
1352 | 						generateRandomString(32, "a-z", "A-Z");
1353 | 					const clientSecret =
1354 | 						options.generateClientSecret?.() ||
1355 | 						generateRandomString(32, "a-z", "A-Z");
1356 | 
1357 | 					const storedClientSecret = await storeClientSecret(ctx, clientSecret);
1358 | 
1359 | 					// Create the client with the existing schema
1360 | 					const client: Client = await ctx.context.adapter.create({
1361 | 						model: modelName.oauthClient,
1362 | 						data: {
1363 | 							name: body.client_name,
1364 | 							icon: body.logo_uri,
1365 | 							metadata: body.metadata ? JSON.stringify(body.metadata) : null,
1366 | 							clientId: clientId,
1367 | 							clientSecret: storedClientSecret,
1368 | 							redirectURLs: body.redirect_uris.join(","),
1369 | 							type: "web",
1370 | 							authenticationScheme:
1371 | 								body.token_endpoint_auth_method || "client_secret_basic",
1372 | 							disabled: false,
1373 | 							userId: session?.session.userId,
1374 | 							createdAt: new Date(),
1375 | 							updatedAt: new Date(),
1376 | 						},
1377 | 					});
1378 | 
1379 | 					// Format the response according to RFC7591
1380 | 					return ctx.json(
1381 | 						{
1382 | 							client_id: clientId,
1383 | 							...(client.type !== "public"
1384 | 								? {
1385 | 										client_secret: clientSecret,
1386 | 										client_secret_expires_at: 0, // 0 means it doesn't expire
1387 | 									}
1388 | 								: {}),
1389 | 							client_id_issued_at: Math.floor(Date.now() / 1000),
1390 | 							client_secret_expires_at: 0, // 0 means it doesn't expire
1391 | 							redirect_uris: body.redirect_uris,
1392 | 							token_endpoint_auth_method:
1393 | 								body.token_endpoint_auth_method || "client_secret_basic",
1394 | 							grant_types: body.grant_types || ["authorization_code"],
1395 | 							response_types: body.response_types || ["code"],
1396 | 							client_name: body.client_name,
1397 | 							client_uri: body.client_uri,
1398 | 							logo_uri: body.logo_uri,
1399 | 							scope: body.scope,
1400 | 							contacts: body.contacts,
1401 | 							tos_uri: body.tos_uri,
1402 | 							policy_uri: body.policy_uri,
1403 | 							jwks_uri: body.jwks_uri,
1404 | 							jwks: body.jwks,
1405 | 							software_id: body.software_id,
1406 | 							software_version: body.software_version,
1407 | 							software_statement: body.software_statement,
1408 | 							metadata: body.metadata,
1409 | 						},
1410 | 						{
1411 | 							status: 201,
1412 | 							headers: {
1413 | 								"Cache-Control": "no-store",
1414 | 								Pragma: "no-cache",
1415 | 							},
1416 | 						},
1417 | 					);
1418 | 				},
1419 | 			),
1420 | 			getOAuthClient: createAuthEndpoint(
1421 | 				"/oauth2/client/:id",
1422 | 				{
1423 | 					method: "GET",
1424 | 					use: [sessionMiddleware],
1425 | 					metadata: {
1426 | 						openapi: {
1427 | 							description: "Get OAuth2 client details",
1428 | 							responses: {
1429 | 								"200": {
1430 | 									description: "OAuth2 client retrieved successfully",
1431 | 									content: {
1432 | 										"application/json": {
1433 | 											schema: {
1434 | 												type: "object",
1435 | 												properties: {
1436 | 													clientId: {
1437 | 														type: "string",
1438 | 														description: "Unique identifier for the client",
1439 | 													},
1440 | 													name: {
1441 | 														type: "string",
1442 | 														description: "Name of the OAuth2 application",
1443 | 													},
1444 | 													icon: {
1445 | 														type: "string",
1446 | 														nullable: true,
1447 | 														description: "Icon URL for the application",
1448 | 													},
1449 | 												},
1450 | 												required: ["clientId", "name"],
1451 | 											},
1452 | 										},
1453 | 									},
1454 | 								},
1455 | 							},
1456 | 						},
1457 | 					},
1458 | 				},
1459 | 				async (ctx) => {
1460 | 					const client = await getClient(
1461 | 						ctx.params.id,
1462 | 						ctx.context.adapter,
1463 | 						trustedClients,
1464 | 					);
1465 | 					if (!client) {
1466 | 						throw new APIError("NOT_FOUND", {
1467 | 							error_description: "client not found",
1468 | 							error: "not_found",
1469 | 						});
1470 | 					}
1471 | 					return ctx.json({
1472 | 						clientId: client.clientId as string,
1473 | 						name: client.name as string,
1474 | 						icon: client.icon as string,
1475 | 					});
1476 | 				},
1477 | 			),
1478 | 		},
1479 | 		schema: mergeSchema(schema, options?.schema),
1480 | 	} satisfies BetterAuthPlugin;
1481 | };
1482 | export type * from "./types";
1483 | 
```
Page 59/69FirstPrevNextLast