From 4e40722c6c95f2cd0fd33524b4c1df6efe0e2342 Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:11:35 +0100 Subject: [PATCH 01/10] feat: implement Better Auth core infrastructure - Add Better Auth and Convex Better Auth packages - Configure convex.config.ts with Better Auth app - Set up auth.config.ts with Google OAuth and anonymous auth - Create auth.ts with client and trigger configuration - Add auth client and server utilities in lib/ - Set up Next.js auth API route handlers This establishes the foundation for migrating from Convex Auth to Better Auth, providing improved flexibility, cleaner APIs, and better TypeScript support. --- app/api/auth/[...all]/route.ts | 5 + bun.lock | 98 ++++++-- convex/auth.config.ts | 3 +- convex/auth.ts | 218 ++++++++++-------- convex/convex.config.ts | 2 + convex/migrations/cleanUserData.ts | 24 ++ .../migrations/removeDuplicateUserFields.ts | 34 +++ lib/auth-client.ts | 11 + lib/auth-server.ts | 7 + package.json | 4 +- 10 files changed, 295 insertions(+), 111 deletions(-) create mode 100644 app/api/auth/[...all]/route.ts create mode 100644 convex/migrations/cleanUserData.ts create mode 100644 convex/migrations/removeDuplicateUserFields.ts create mode 100644 lib/auth-client.ts create mode 100644 lib/auth-server.ts diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..15a37fa --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,5 @@ +import { nextJsHandler } from "@convex-dev/better-auth/nextjs"; + +// Export GET and POST handlers for Better Auth +// This handles all auth routes like /api/auth/signin, /api/auth/callback/google, etc. +export const { GET, POST } = nextJsHandler(); diff --git a/bun.lock b/bun.lock index d78322b..3367f38 100644 --- a/bun.lock +++ b/bun.lock @@ -10,11 +10,10 @@ "@ai-sdk/mistral": "^2.0.14", "@ai-sdk/openai": "^2.0.32", "@ai-sdk/openai-compatible": "^1.0.18", - "@ai-sdk/react": "^2.0.47", - "@auth/core": "0.40.0", + "@ai-sdk/react": "^2.0.48", "@composio/core": "^0.1.52", "@composio/vercel": "^0.2.8", - "@convex-dev/auth": "^0.0.89", + "@convex-dev/better-auth": "^0.8.6", "@convex-dev/polar": "^0.6.4", "@convex-dev/r2": "^0.7.3", "@convex-dev/rate-limiter": "^0.2.12", @@ -49,8 +48,9 @@ "@upstash/redis": "^1.35.4", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", - "ai": "^5.0.47", + "ai": "^5.0.48", "babel-plugin-react-compiler": "^19.1.0-rc.3", + "better-auth": "1.3.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -69,7 +69,7 @@ "posthog-js": "^1.266.3", "posthog-node": "^5.8.6", "react": "19.1.1", - "react-day-picker": "^9.10.0", + "react-day-picker": "^9.11.0", "react-dom": "19.1.1", "react-markdown": "^10.1.0", "rehype-katex": "^7.0.1", @@ -157,7 +157,7 @@ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], - "@auth/core": ["@auth/core@0.40.0", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw=="], + "@auth/core": ["@auth/core@0.37.4", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^5.9.6", "oauth4webapi": "^3.1.1", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -281,6 +281,10 @@ "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + "@better-auth/utils": ["@better-auth/utils@0.2.6", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], + "@biomejs/biome": ["@biomejs/biome@2.2.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.2", "@biomejs/cli-darwin-x64": "2.2.2", "@biomejs/cli-linux-arm64": "2.2.2", "@biomejs/cli-linux-arm64-musl": "2.2.2", "@biomejs/cli-linux-x64": "2.2.2", "@biomejs/cli-linux-x64-musl": "2.2.2", "@biomejs/cli-win32-arm64": "2.2.2", "@biomejs/cli-win32-x64": "2.2.2" }, "bin": { "biome": "bin/biome" } }, "sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ=="], @@ -329,7 +333,9 @@ "@convex-dev/action-retrier": ["@convex-dev/action-retrier@0.2.1", "", { "peerDependencies": { "convex": "~1.16.5 || >=1.17.0 <1.35.0" } }, "sha512-yxc2ECtgFAAync8Yf1ykh1nzgXkXjrCA5mFYB0lRq/Dgx1TFyFuYfw10ECn7v9/aMUjzUpyJEJp9vRscrfFsng=="], - "@convex-dev/auth": ["@convex-dev/auth@0.0.89", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "cookie": "^1.0.1", "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", "oauth4webapi": "^3.1.2", "path-to-regexp": "^6.3.0", "server-only": "^0.0.1" }, "peerDependencies": { "@auth/core": "^0.37.0", "convex": "^1.17.0", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["react"], "bin": { "auth": "dist/bin.cjs" } }, "sha512-s6E4Q295cxvh2pWvkKQ8PhUKq8OXzK8eENJ/ZOuNRY1K0AEl7RSnldsDEl0PTbCbo3jNVtiV/ol0YeRfoCNrKg=="], + "@convex-dev/auth": ["@convex-dev/auth@0.0.83", "", { "dependencies": { "cookie": "^1.0.1", "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", "oauth4webapi": "^3.1.2", "oslo": "^1.1.2", "path-to-regexp": "^6.3.0", "server-only": "^0.0.1" }, "peerDependencies": { "@auth/core": "^0.37.0", "convex": "^1.17.0", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["react"], "bin": { "auth": "dist/bin.cjs" } }, "sha512-KJPI9x3U1KcHNE/7pA7B92sVsHWpAfg6vWuO005OWNF359eRJ3ZP3W+iNfuMajZLqywudR2+oVoGDnBIFrM63A=="], + + "@convex-dev/better-auth": ["@convex-dev/better-auth@0.8.6", "", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "is-network-error": "^1.1.0", "type-fest": "^4.39.1", "zod": "^3.24.4" }, "peerDependencies": { "better-auth": "1.3.8", "convex": "^1.26.2", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-iP2erc4YKz0HzrWSdaSRv2ck3gsdb2yBwUdSOsM0ebvNMcTsoWEBuu07cq6lUGMyJ97qxOnZDtN7CemTXKpSpA=="], "@convex-dev/polar": ["@convex-dev/polar@0.6.4", "", { "dependencies": { "buffer": "^6.0.3", "convex-helpers": "^0.1.63", "remeda": "^2.20.2", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "@polar-sh/checkout": ">=0.1.10", "@polar-sh/sdk": ">=0.32.11", "convex": "^1.25.4", "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-QXWe+URjSMIkJI9vB8u4ghNA3IPa1fCnlhP3lXg0kDV5CvW7imZx7TzXx2Yce9ZQA7EhMfSJH6GNBV7zYa7vhQ=="], @@ -465,6 +471,8 @@ "@google/genai": ["@google/genai@1.20.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.4" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-QdShxO9LX35jFogy3iKprQNqgKKveux4H2QjOnyIvyHRuGi6PHiz3fjNf8Y0VPY8o5V2fHqR2XqiSVoz7yZs0w=="], + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], "@iconify/utils": ["@iconify/utils@3.0.2", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@antfu/utils": "^9.2.0", "@iconify/types": "^2.0.0", "debug": "^4.4.1", "globals": "^15.15.0", "kolorist": "^1.8.0", "local-pkg": "^1.1.1", "mlly": "^1.7.4" } }, "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ=="], @@ -533,6 +541,8 @@ "@langchain/textsplitters": ["@langchain/textsplitters@0.1.0", "", { "dependencies": { "js-tiktoken": "^1.0.12" }, "peerDependencies": { "@langchain/core": ">=0.2.21 <0.4.0" } }, "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.4.0", "", {}, "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw=="], "@lit/reactive-element": ["@lit/reactive-element@2.1.1", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.4.0" } }, "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg=="], @@ -571,6 +581,10 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw=="], + "@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@node-rs/argon2": ["@node-rs/argon2@1.7.0", "", { "optionalDependencies": { "@node-rs/argon2-android-arm-eabi": "1.7.0", "@node-rs/argon2-android-arm64": "1.7.0", "@node-rs/argon2-darwin-arm64": "1.7.0", "@node-rs/argon2-darwin-x64": "1.7.0", "@node-rs/argon2-freebsd-x64": "1.7.0", "@node-rs/argon2-linux-arm-gnueabihf": "1.7.0", "@node-rs/argon2-linux-arm64-gnu": "1.7.0", "@node-rs/argon2-linux-arm64-musl": "1.7.0", "@node-rs/argon2-linux-x64-gnu": "1.7.0", "@node-rs/argon2-linux-x64-musl": "1.7.0", "@node-rs/argon2-wasm32-wasi": "1.7.0", "@node-rs/argon2-win32-arm64-msvc": "1.7.0", "@node-rs/argon2-win32-ia32-msvc": "1.7.0", "@node-rs/argon2-win32-x64-msvc": "1.7.0" } }, "sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog=="], "@node-rs/argon2-android-arm-eabi": ["@node-rs/argon2-android-arm-eabi@1.7.0", "", { "os": "android", "cpu": "arm" }, "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg=="], @@ -645,6 +659,30 @@ "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="], + + "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A=="], + + "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg=="], + + "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug=="], + + "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw=="], + + "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pfx": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A=="], + + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.5.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ=="], + + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ=="], + + "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A=="], + + "@peculiar/x509": ["@peculiar/x509@1.14.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-csr": "^2.5.0", "@peculiar/asn1-ecc": "^2.5.0", "@peculiar/asn1-pkcs9": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg=="], + "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], "@polar-sh/checkout": ["@polar-sh/checkout@0.1.12", "", { "dependencies": { "@polar-sh/sdk": "^0.34.9", "@polar-sh/ui": "^0.1.1", "event-source-plus": "^0.1.11", "eventemitter3": "^5.0.1", "markdown-to-jsx": "^7.7.12", "react-hook-form": "^7.60.0" }, "peerDependencies": { "@stripe/react-stripe-js": "^3.6.0", "@stripe/stripe-js": "^7.1.0", "react": "^18 || ^19" } }, "sha512-CmNdrZKOnr22Z2Cj0yeD0VfxeHW4eJufHjdufORBZwjoSnr9/xkUm+mdGIBlTGZRfAC2AekQ/ie8aqx9PVWfLQ=="], @@ -841,6 +879,10 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.0", "", {}, "sha512-N3fuA1AAnTo5gCStYoIoiasPccC+xPLx2YU88Dv0GeAmPQTWHETlZQq5xZ0DgUq1H9loXMWQH5qqUjcI7BHJ1A=="], + + "@simplewebauthn/server": ["@simplewebauthn/server@13.2.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-Inmfye5opZXe3HI0GaksqBnQiM7glcNySoG6DH1GgkO1Lh9dvuV4XSV9DK02DReUVX39HpcDob9nxHELjECoQw=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.1.1", "", { "dependencies": { "@smithy/types": "^4.5.0", "tslib": "^2.6.2" } }, "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg=="], "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-a36AtR7Q7XOhRPt6F/7HENmTWcB8kN7mDJcOFM/+FuKO6x88w8MQJfYCufMWh4fGyVkPjUh3Rrz/dnqFQdo6OQ=="], @@ -1171,6 +1213,8 @@ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="], @@ -1197,6 +1241,10 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.8.6", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw=="], + "better-auth": ["better-auth@1.3.8", "", { "dependencies": { "@better-auth/utils": "0.2.6", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.8.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.16", "defu": "^6.1.4", "jose": "^5.10.0", "kysely": "^0.28.5", "nanostores": "^0.11.4", "zod": "^4.1.5" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-uRFzHbWkhr8eWNy+BJwyMnrZPOvQjwrcLND3nc6jusRteYA9cjeRGElgCPTWTIyWUfzaQ708Lb5Mdq9Gv41Qpw=="], + + "better-call": ["better-call@1.0.16", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-42dgJ1rOtc0anOoxjXPOWuel/Z/4aeO7EJ2SiXNwvlkySSgjXhNjAjTMWa8DL1nt6EXS3jl3VKC3mPsU/lUgVA=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -1265,6 +1313,8 @@ "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], @@ -1403,6 +1453,8 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -1637,7 +1689,7 @@ "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], - "jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="], + "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], @@ -1677,6 +1729,8 @@ "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + "kysely": ["kysely@0.28.7", "", {}, "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw=="], + "langchain": ["langchain@0.3.34", "", { "dependencies": { "@langchain/openai": ">=0.1.0 <0.7.0", "@langchain/textsplitters": ">=0.0.0 <0.2.0", "js-tiktoken": "^1.0.12", "js-yaml": "^4.1.0", "jsonpointer": "^5.0.1", "langsmith": "^0.3.67", "openapi-types": "^12.1.3", "p-retry": "4", "uuid": "^10.0.0", "yaml": "^2.2.1", "zod": "^3.25.32" }, "peerDependencies": { "@langchain/anthropic": "*", "@langchain/aws": "*", "@langchain/cerebras": "*", "@langchain/cohere": "*", "@langchain/core": ">=0.3.58 <0.4.0", "@langchain/deepseek": "*", "@langchain/google-genai": "*", "@langchain/google-vertexai": "*", "@langchain/google-vertexai-web": "*", "@langchain/groq": "*", "@langchain/mistralai": "*", "@langchain/ollama": "*", "@langchain/xai": "*", "axios": "*", "cheerio": "*", "handlebars": "^4.7.8", "peggy": "^3.0.2", "typeorm": "*" }, "optionalPeers": ["@langchain/anthropic", "@langchain/aws", "@langchain/cerebras", "@langchain/cohere", "@langchain/deepseek", "@langchain/google-genai", "@langchain/google-vertexai", "@langchain/google-vertexai-web", "@langchain/groq", "@langchain/mistralai", "@langchain/ollama", "@langchain/xai", "axios", "cheerio", "handlebars", "peggy", "typeorm"] }, "sha512-OADHLQYRX+36EqQBxIoryCdMKfHex32cJBSWveadIIeRhygqivacIIDNwVjX51Y++c80JIdR0jaQHWn2r3H1iA=="], "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], @@ -1895,6 +1949,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="], + "next": ["next@15.5.3", "", { "dependencies": { "@next/env": "15.5.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.3", "@next/swc-darwin-x64": "15.5.3", "@next/swc-linux-arm64-gnu": "15.5.3", "@next/swc-linux-arm64-musl": "15.5.3", "@next/swc-linux-x64-gnu": "15.5.3", "@next/swc-linux-x64-musl": "15.5.3", "@next/swc-win32-arm64-msvc": "15.5.3", "@next/swc-win32-x64-msvc": "15.5.3", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -1991,6 +2047,10 @@ "pusher-js": ["pusher-js@8.4.0", "", { "dependencies": { "tweetnacl": "^1.0.3" } }, "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q=="], + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], "query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="], @@ -2119,6 +2179,8 @@ "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -2169,6 +2231,8 @@ "rollup": ["rollup@4.52.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.0", "@rollup/rollup-android-arm64": "4.52.0", "@rollup/rollup-darwin-arm64": "4.52.0", "@rollup/rollup-darwin-x64": "4.52.0", "@rollup/rollup-freebsd-arm64": "4.52.0", "@rollup/rollup-freebsd-x64": "4.52.0", "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", "@rollup/rollup-linux-arm-musleabihf": "4.52.0", "@rollup/rollup-linux-arm64-gnu": "4.52.0", "@rollup/rollup-linux-arm64-musl": "4.52.0", "@rollup/rollup-linux-loong64-gnu": "4.52.0", "@rollup/rollup-linux-ppc64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-musl": "4.52.0", "@rollup/rollup-linux-s390x-gnu": "4.52.0", "@rollup/rollup-linux-x64-gnu": "4.52.0", "@rollup/rollup-linux-x64-musl": "4.52.0", "@rollup/rollup-openharmony-arm64": "4.52.0", "@rollup/rollup-win32-arm64-msvc": "4.52.0", "@rollup/rollup-win32-ia32-msvc": "4.52.0", "@rollup/rollup-win32-x64-gnu": "4.52.0", "@rollup/rollup-win32-x64-msvc": "4.52.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g=="], + "rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], @@ -2193,6 +2257,8 @@ "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + "set-value": ["set-value@2.0.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" } }, "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw=="], "sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="], @@ -2315,6 +2381,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -2463,12 +2531,6 @@ "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@convex-dev/auth/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], - - "@convex-dev/react-query/@auth/core": ["@auth/core@0.37.4", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^5.9.6", "oauth4webapi": "^3.1.1", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw=="], - - "@convex-dev/react-query/@convex-dev/auth": ["@convex-dev/auth@0.0.83", "", { "dependencies": { "cookie": "^1.0.1", "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", "oauth4webapi": "^3.1.2", "oslo": "^1.1.2", "path-to-regexp": "^6.3.0", "server-only": "^0.0.1" }, "peerDependencies": { "@auth/core": "^0.37.0", "convex": "^1.17.0", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["react"], "bin": { "auth": "dist/bin.cjs" } }, "sha512-KJPI9x3U1KcHNE/7pA7B92sVsHWpAfg6vWuO005OWNF359eRJ3ZP3W+iNfuMajZLqywudR2+oVoGDnBIFrM63A=="], - "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], @@ -2531,6 +2593,8 @@ "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "better-auth/zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], + "cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], @@ -2601,6 +2665,8 @@ "trpc-cli/commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="], + "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "ultracite/zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], "webpack-bundle-analyzer/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], @@ -2619,10 +2685,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@convex-dev/react-query/@auth/core/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], - - "@convex-dev/react-query/@convex-dev/auth/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], - "@posthog/ai/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.9" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-Mwp0yYXrEnENoDrc7IH9yVRVJ7RrDW0CXWDtyz1BiyqccbtdWhAKu4wtrDMx2FkeK5riiME1kYYdjRnlba3UFw=="], "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], diff --git a/convex/auth.config.ts b/convex/auth.config.ts index f4eb564..0cf2367 100644 --- a/convex/auth.config.ts +++ b/convex/auth.config.ts @@ -1,7 +1,8 @@ export default { providers: [ { - domain: process.env.CONVEX_SITE_URL, + domain: + process.env.CONVEX_SITE_URL || process.env.NEXT_PUBLIC_CONVEX_SITE_URL, applicationID: "convex", }, ], diff --git a/convex/auth.ts b/convex/auth.ts index d789b70..4cb8d8a 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -1,106 +1,144 @@ -import Google from "@auth/core/providers/google"; -import { Anonymous } from "@convex-dev/auth/providers/Anonymous"; -import { convexAuth } from "@convex-dev/auth/server"; +import { + type AuthFunctions, + createClient, + type GenericCtx, +} from "@convex-dev/better-auth"; +import { convex } from "@convex-dev/better-auth/plugins"; +import { betterAuth } from "better-auth"; +import { anonymous } from "better-auth/plugins"; import { MODEL_DEFAULT, RECOMMENDED_MODELS } from "../lib/config"; -import type { Id } from "./_generated/dataModel"; -import type { MutationCtx } from "./_generated/server"; +import { components, internal } from "./_generated/api"; +import type { DataModel, Id } from "./_generated/dataModel"; import { rateLimiter } from "./rateLimiter"; -// Helper function to initialize user fields -const initializeUserFields = () => ({ - preferredModel: MODEL_DEFAULT, - // By default no models are disabled – an empty array means all are enabled - disabledModels: [], - // Initialize with recommended models as favorites - favoriteModels: [...RECOMMENDED_MODELS], -}); - -// Helper function to initialize rate limits for new user -const initializeRateLimits = async ( - ctx: MutationCtx, - userId: Id<"users">, - isAnonymous: boolean -): Promise => { - try { - const rateLimitPromises: Promise[] = []; - - // Daily limits based on user type - const dailyLimitName = isAnonymous - ? "anonymousDaily" - : "authenticatedDaily"; - rateLimitPromises.push( - rateLimiter.limit(ctx, dailyLimitName, { - key: userId, - count: 0, - }) - ); +const siteUrl = process.env.SITE_URL || "http://localhost:3000"; - // Monthly limits for all users - rateLimitPromises.push( - rateLimiter.limit(ctx, "standardMonthly", { - key: userId, - count: 0, - }) - ); +// Auth functions for Better Auth internal use +const authFunctions: AuthFunctions = internal.auth; - // Initialize premium credits counter for all users (will only be used by premium users) - rateLimitPromises.push( - rateLimiter.limit(ctx, "premiumMonthly", { - key: userId, - count: 0, - }) - ); +// The component client has methods needed for integrating Convex with Better Auth, +// as well as helper methods for general use. +export const authComponent = createClient(components.betterAuth, { + verbose: false, + authFunctions, + triggers: { + user: { + onCreate: async (ctx, authUser) => { + // CRITICAL: Check if user already exists by email to preserve existing data + // This is essential for production users with subscriptions and chat history + if (authUser.email) { + const existingUser = await ctx.db + .query("users") + .filter((q) => q.eq(q.field("email"), authUser.email)) + .first(); - await Promise.all(rateLimitPromises); - } catch (_error) { - // Non-fatal: rate-limit initialisation failure should never block the - // user flow. The rate-limiter will lazily create windows on first use. - } -}; - -export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ - providers: [ - Google, - Anonymous({ - profile: () => ({ isAnonymous: true }), - }), - ], - callbacks: { - async createOrUpdateUser(ctx, args) { - const { existingUserId, type, profile } = args; + if (existingUser) { + // Link Better Auth user to existing user (preserves subscriptions and data!) + await authComponent.setUserId(ctx, authUser._id, existingUser._id); + return; + } + } - // If user already exists, just return their ID - no updates needed - if (existingUserId) { - return existingUserId; - } + // Only create new user if no existing one found + // Create new user in our users table with app-specific preferences only + // Identity fields (name, email, image, isAnonymous) are managed by Better Auth + const userId = await ctx.db.insert("users", { + // Initialize user preferences only + preferredModel: MODEL_DEFAULT, + disabledModels: [], + favoriteModels: [...RECOMMENDED_MODELS], + }); - // Create new user (either anonymous or OAuth) - const isAnonymous = type !== "oauth"; + // Link Better Auth user to our user + await authComponent.setUserId(ctx, authUser._id, userId); - // Build user fields with OAuth profile information if available - const baseFields = { - isAnonymous, - ...initializeUserFields(), - }; + // Initialize rate limits for new user + try { + const rateLimitPromises: Promise[] = []; - const userFields = - type === "oauth" && profile - ? { - ...baseFields, - name: profile.name as string | undefined, - email: profile.email as string | undefined, - image: (profile.picture || profile.image) as string | undefined, - // OAuth providers have already verified the email - emailVerificationTime: Date.now(), - } - : baseFields; + // Daily limits based on user type from Better Auth + const isAnonymous = authUser.isAnonymous ?? false; + const dailyLimitName = isAnonymous + ? "anonymousDaily" + : "authenticatedDaily"; + rateLimitPromises.push( + rateLimiter.limit(ctx, dailyLimitName, { + key: userId, + count: 0, + }) + ); - const userId = await ctx.db.insert("users", userFields); + // Monthly limits for all users + rateLimitPromises.push( + rateLimiter.limit(ctx, "standardMonthly", { + key: userId, + count: 0, + }) + ); - // Initialize rate limits for new user - await initializeRateLimits(ctx, userId, isAnonymous); + // Initialize premium credits counter for all users (will only be used by premium users) + rateLimitPromises.push( + rateLimiter.limit(ctx, "premiumMonthly", { + key: userId, + count: 0, + }) + ); - return userId; + await Promise.all(rateLimitPromises); + } catch (_error) { + // Non-fatal: rate-limit initialization failure should never block the user flow + } + }, + onUpdate: async (_ctx, _oldUser, _newUser) => { + // Handle user updates if needed + }, + onDelete: async (ctx, authUser) => { + // Clean up when user is deleted + if (authUser.userId) { + await ctx.db.delete(authUser.userId as Id<"users">); + } + }, }, }, }); + +export const createAuth = ( + ctx: GenericCtx, + { optionsOnly } = { optionsOnly: false } +) => { + return betterAuth({ + // Disable logging when createAuth is called just to generate options. + // This is not required, but there's a lot of noise in logs without it. + logger: { + disabled: optionsOnly, + }, + baseURL: siteUrl, + database: authComponent.adapter(ctx), + socialProviders: { + google: { + clientId: process.env.AUTH_GOOGLE_ID || "", + clientSecret: process.env.AUTH_GOOGLE_SECRET || "", + }, + }, + session: { + cookieCache: { + enabled: true, + maxAge: 60 * 60, // Cache duration in seconds (1 hour) + }, + }, + plugins: [ + // Anonymous authentication + anonymous({ + onLinkAccount: () => { + // When anonymous user links to Google account, + // the onCreate trigger will handle merging the data + }, + }), + // The Convex plugin is required for Convex compatibility (NO crossDomain for Next.js!) + convex(), + ], + }); +}; + +// Export trigger functions for use in other Convex functions +export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi(); diff --git a/convex/convex.config.ts b/convex/convex.config.ts index 03a2fbe..a3fe740 100644 --- a/convex/convex.config.ts +++ b/convex/convex.config.ts @@ -1,3 +1,4 @@ +import betterAuth from "@convex-dev/better-auth/convex.config"; import polar from "@convex-dev/polar/convex.config"; import r2 from "@convex-dev/r2/convex.config"; import rateLimiter from "@convex-dev/rate-limiter/convex.config"; @@ -9,5 +10,6 @@ app.use(rateLimiter); app.use(polar); app.use(resend); app.use(r2); +app.use(betterAuth); export default app; diff --git a/convex/migrations/cleanUserData.ts b/convex/migrations/cleanUserData.ts new file mode 100644 index 0000000..80a9430 --- /dev/null +++ b/convex/migrations/cleanUserData.ts @@ -0,0 +1,24 @@ +import { internalMutation } from "../_generated/server"; + +export const cleanUserData = internalMutation({ + args: {}, + handler: async (ctx) => { + // Get all users + const allUsers = await ctx.db.query("users").collect(); + + // Update each user to remove duplicate fields that Better Auth now manages + const updatePromises = allUsers.map(async (user) => { + // Remove fields that will be managed by Better Auth using ctx.db.patch + await ctx.db.patch(user._id, { + name: undefined, // Better Auth manages this + image: undefined, // Better Auth manages this + isAnonymous: undefined, // Better Auth manages this + emailVerificationTime: undefined, // Better Auth manages this + }); + }); + + await Promise.all(updatePromises); + + return { processedUsers: allUsers.length }; + }, +}); diff --git a/convex/migrations/removeDuplicateUserFields.ts b/convex/migrations/removeDuplicateUserFields.ts new file mode 100644 index 0000000..9ebad90 --- /dev/null +++ b/convex/migrations/removeDuplicateUserFields.ts @@ -0,0 +1,34 @@ +import { internalMutation } from "../_generated/server"; + +export const removeDuplicateUserFields = internalMutation({ + args: {}, + handler: async (ctx) => { + // Get all users + const allUsers = await ctx.db.query("users").collect(); + + // Update each user to remove duplicate fields + const updatePromises = allUsers.map(async (user) => { + // Keep only the fields we want in the custom table + const cleanedUser = { + // Keep app-specific fields only + preferredModel: user.preferredModel, + preferredName: user.preferredName, + occupation: user.occupation, + traits: user.traits, + about: user.about, + disabledModels: user.disabledModels || [], + favoriteModels: user.favoriteModels || [], + // Keep email temporarily for migration + email: user.email, + }; + + // Remove fields that will be managed by Better Auth + // This will remove: name, image, isAnonymous, emailVerificationTime + await ctx.db.replace(user._id, cleanedUser); + }); + + await Promise.all(updatePromises); + + return { processedUsers: allUsers.length }; + }, +}); diff --git a/lib/auth-client.ts b/lib/auth-client.ts new file mode 100644 index 0000000..0240f9c --- /dev/null +++ b/lib/auth-client.ts @@ -0,0 +1,11 @@ +import { convexClient } from "@convex-dev/better-auth/client/plugins"; +import { anonymousClient } from "better-auth/client/plugins"; +import { createAuthClient } from "better-auth/react"; + +// Create the Better Auth client for frontend use +export const authClient = createAuthClient({ + plugins: [ + convexClient(), // Convex integration + anonymousClient(), // Anonymous authentication support + ], +}); diff --git a/lib/auth-server.ts b/lib/auth-server.ts new file mode 100644 index 0000000..aa0a227 --- /dev/null +++ b/lib/auth-server.ts @@ -0,0 +1,7 @@ +import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs"; +import { createAuth } from "../convex/auth"; + +// Helper function to get authentication token for server-side operations +export const getToken = () => { + return getTokenNextjs(createAuth); +}; diff --git a/package.json b/package.json index b281225..620badb 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,9 @@ "@ai-sdk/openai": "^2.0.32", "@ai-sdk/openai-compatible": "^1.0.18", "@ai-sdk/react": "^2.0.48", - "@auth/core": "0.40.0", "@composio/core": "^0.1.52", "@composio/vercel": "^0.2.8", - "@convex-dev/auth": "^0.0.89", + "@convex-dev/better-auth": "^0.8.6", "@convex-dev/polar": "^0.6.4", "@convex-dev/r2": "^0.7.3", "@convex-dev/rate-limiter": "^0.2.12", @@ -61,6 +60,7 @@ "@vercel/speed-insights": "^1.2.0", "ai": "^5.0.48", "babel-plugin-react-compiler": "^19.1.0-rc.3", + "better-auth": "1.3.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", From af2d5dea101bb08c3b041a94533fe8a8e395d014 Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:12:28 +0100 Subject: [PATCH 02/10] feat: update frontend providers for Better Auth - Replace ConvexReactClient with ConvexBetterAuthProvider - Integrate authClient from lib/auth-client.ts - Update user provider to use Better Auth hooks and methods - Configure layout to use new auth provider structure This establishes the React context and providers needed for Better Auth integration while maintaining existing user experience patterns. --- app/layout.tsx | 22 ++++++------- app/providers/convex-client-provider.tsx | 12 +++++--- app/providers/user-provider.tsx | 39 ++++++++++++++---------- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index d4dc0ea..0689c48 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -132,8 +132,6 @@ export const metadata: Metadata = { }, }; -import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server"; - export default function RootLayout({ children, }: Readonly<{ @@ -164,17 +162,15 @@ export default function RootLayout({ )} - - - - - {children} - - - - - - + + + + {children} + + + + + diff --git a/app/providers/convex-client-provider.tsx b/app/providers/convex-client-provider.tsx index d28c367..1e6a376 100644 --- a/app/providers/convex-client-provider.tsx +++ b/app/providers/convex-client-provider.tsx @@ -1,10 +1,11 @@ "use client"; -import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs"; +import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; import { ConvexQueryClient } from "@convex-dev/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ConvexReactClient } from "convex/react"; import { useState } from "react"; +import { authClient } from "@/lib/auth-client"; // Validate environment variable early for clearer error messaging const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; @@ -15,7 +16,10 @@ if (!convexUrl) { ); } -const client = new ConvexReactClient(convexUrl); +const client = new ConvexReactClient(convexUrl, { + expectAuth: false, + verbose: false, +}); export function ConvexClientProvider({ children, @@ -44,11 +48,11 @@ export function ConvexClientProvider({ }); return ( - + {children} {process.env.NODE_ENV === "development" && } - + ); } diff --git a/app/providers/user-provider.tsx b/app/providers/user-provider.tsx index 7c4638d..9edacd3 100644 --- a/app/providers/user-provider.tsx +++ b/app/providers/user-provider.tsx @@ -1,9 +1,9 @@ "use client"; -import { useAuthActions } from "@convex-dev/auth/react"; import { convexQuery } from "@convex-dev/react-query"; import { useQuery as useTanStackQuery } from "@tanstack/react-query"; import { useMutation } from "convex/react"; import { createContext, useCallback, useContext, useMemo } from "react"; +import { authClient } from "@/lib/auth-client"; import { api } from "../../convex/_generated/api"; import type { Doc, Id } from "../../convex/_generated/dataModel"; @@ -67,8 +67,7 @@ export function UserProvider({ children: React.ReactNode; initialUser?: null; }) { - // isLoading will always be false here - const { signIn, signOut } = useAuthActions(); + // Auth actions from Better Auth const { data: user = null, isLoading: isUserLoading } = useTanStackQuery({ ...convexQuery(api.users.getCurrentUser, {}), // Extended cache for user data to prevent auth flickering @@ -85,30 +84,35 @@ export function UserProvider({ // and proper error handling rather than preventing the subscription entirely. const { data: hasPremium, isLoading: isPremiumLoading } = useTanStackQuery({ - ...convexQuery(api.users.userHasPremium, {}), - enabled: Boolean(user) && !user?.isAnonymous, + ...convexQuery( + api.users.userHasPremium, + user && !user?.isAnonymous ? {} : "skip" + ), // Premium status changes infrequently, cache longer gcTime: 15 * 60 * 1000, // 15 minutes }); const { data: products, isLoading: isProductsLoading } = useTanStackQuery({ - ...convexQuery(api.polar.getConfiguredProducts, {}), - enabled: Boolean(user) && !user?.isAnonymous, + ...convexQuery( + api.polar.getConfiguredProducts, + user && !user?.isAnonymous ? {} : "skip" + ), // Product configurations are very stable, cache aggressively gcTime: 60 * 60 * 1000, // 60 minutes }); const { data: rateLimitStatus, isLoading: isRateLimitLoading } = useTanStackQuery({ - ...convexQuery(api.users.getRateLimitStatus, {}), - enabled: Boolean(user), + ...convexQuery(api.users.getRateLimitStatus, user ? {} : "skip"), // Rate limits update more frequently, shorter cache gcTime: 5 * 60 * 1000, // 5 minutes }); const { data: apiKeysQuery, isLoading: isApiKeysLoading } = useTanStackQuery({ - ...convexQuery(api.api_keys.getApiKeys, {}), - enabled: Boolean(user) && !user?.isAnonymous, + ...convexQuery( + api.api_keys.getApiKeys, + user && !user?.isAnonymous ? {} : "skip" + ), // API keys are relatively stable, cache reasonably gcTime: 10 * 60 * 1000, // 10 minutes }); @@ -119,7 +123,6 @@ export function UserProvider({ api.connectors.listUserConnectors, user && !user?.isAnonymous ? {} : "skip" ), - enabled: Boolean(user) && !user?.isAnonymous, // Connectors are relatively stable, cache reasonably gcTime: 10 * 60 * 1000, // 10 minutes }); @@ -129,8 +132,8 @@ export function UserProvider({ // User creation and account linking is handled by the createOrUpdateUser callback in auth.ts const signInGoogle = useCallback(async () => { - await signIn("google"); - }, [signIn]); + await authClient.signIn.social({ provider: "google" }); + }, []); const updateUser = useCallback( async (updates: Partial) => { @@ -178,12 +181,16 @@ export function UserProvider({ isConnectorsLoading)) ); + const signOutUser = useCallback(async () => { + await authClient.signOut(); + }, []); + const contextValue = useMemo( () => ({ user, isLoading: combinedLoading, signInGoogle, - signOut, + signOut: signOutUser, updateUser, // User capabilities and settings hasPremium: (hasPremium ?? false) as boolean, @@ -221,7 +228,7 @@ export function UserProvider({ user, combinedLoading, signInGoogle, - signOut, + signOutUser, updateUser, hasPremium, products, From 70ffcb10c0528c3c3befeb052dc04125f1acdf94 Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:13:22 +0100 Subject: [PATCH 03/10] feat: update auth components for Better Auth API - Replace signIn("anonymous") with authClient.signIn.anonymous() - Replace signIn("google") with authClient.signIn.social({ provider: "google" }) - Update auth dialogs and popovers to use Better Auth methods - Migrate auth page to use new Better Auth client patterns This updates all user-facing authentication components to use the new Better Auth API while maintaining the same user experience. --- app/auth/page.tsx | 8 +++--- app/components/auth/anonymous-sign-in.tsx | 28 +++++++++++++++---- .../chat-input/popover-content-auth.tsx | 6 ++-- app/components/chat/dialog-auth.tsx | 8 +++--- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/app/auth/page.tsx b/app/auth/page.tsx index 1716e8f..8ba8b2f 100644 --- a/app/auth/page.tsx +++ b/app/auth/page.tsx @@ -1,11 +1,11 @@ "use client"; -import { useAuthActions } from "@convex-dev/auth/react"; import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { TextHoverEffect } from "@/components/ui/text-hover-effect"; +import { authClient } from "@/lib/auth-client"; import { APP_NAME } from "@/lib/config"; import { HeaderGoBack } from "../components/header-go-back"; @@ -13,14 +13,14 @@ export default function LoginPage() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const { signIn } = useAuthActions(); - async function handleSignInWithGoogle() { try { setIsLoading(true); setError(null); - await signIn("google"); + await authClient.signIn.social({ + provider: "google", + }); } catch (err: unknown) { setIsLoading(false); // console.error('Error signing in with Google:', err); diff --git a/app/components/auth/anonymous-sign-in.tsx b/app/components/auth/anonymous-sign-in.tsx index cccf0a9..8b1a66c 100644 --- a/app/components/auth/anonymous-sign-in.tsx +++ b/app/components/auth/anonymous-sign-in.tsx @@ -1,20 +1,38 @@ "use client"; -import { useAuthActions } from "@convex-dev/auth/react"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { Loader } from "@/components/prompt-kit/loader"; +import { authClient } from "@/lib/auth-client"; export function AnonymousSignIn() { - const { signIn } = useAuthActions(); const attemptedAnon = useRef(false); + const [error, setError] = useState(null); // Handle anonymous sign-in when user is unauthenticated useEffect(() => { if (!attemptedAnon.current) { attemptedAnon.current = true; - signIn("anonymous"); + authClient.signIn.anonymous().catch(() => { + setError("Failed to sign in. Please refresh the page."); + }); } - }, [signIn]); + }, []); + + // Show error if sign-in fails + if (error) { + return ( +
+

{error}

+ +
+ ); + } // Show loading while anonymous sign-in is processing return ( diff --git a/app/components/chat-input/popover-content-auth.tsx b/app/components/chat-input/popover-content-auth.tsx index a529dad..a857703 100644 --- a/app/components/chat-input/popover-content-auth.tsx +++ b/app/components/chat-input/popover-content-auth.tsx @@ -1,24 +1,22 @@ "use client"; -import { useAuthActions } from "@convex-dev/auth/react"; import Image from "next/image"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { PopoverContent } from "@/components/ui/popover"; +import { authClient } from "@/lib/auth-client"; import { APP_NAME } from "../../../lib/config"; export function PopoverContentAuth() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const { signIn } = useAuthActions(); - const handleSignInWithGoogle = async () => { try { setIsLoading(true); setError(null); - await signIn("google"); + await authClient.signIn.social({ provider: "google" }); } catch (_err: unknown) { // console.error('Error signing in with Google:', err); setError("Unable to sign in at the moment. Please try again later."); diff --git a/app/components/chat/dialog-auth.tsx b/app/components/chat/dialog-auth.tsx index 6f35bdf..45065b1 100644 --- a/app/components/chat/dialog-auth.tsx +++ b/app/components/chat/dialog-auth.tsx @@ -1,6 +1,5 @@ "use client"; -import { useAuthActions } from "@convex-dev/auth/react"; import Image from "next/image"; import { useState } from "react"; import { Button } from "@/components/ui/button"; @@ -12,6 +11,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { authClient } from "@/lib/auth-client"; type DialogAuthProps = { open: boolean; @@ -22,13 +22,13 @@ export function DialogAuth({ open, setOpen }: DialogAuthProps) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const { signIn } = useAuthActions(); - const handleSignInWithGoogle = async () => { try { setIsLoading(true); setError(null); - await signIn("google"); + await authClient.signIn.social({ + provider: "google", + }); setOpen(false); } catch (err: unknown) { if (err instanceof Error) { From 8870dc87d5f8cd5fbf38900958c44a0fb9974739 Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:13:48 +0100 Subject: [PATCH 04/10] feat: update API routes for Better Auth integration - Update chat API route to use Better Auth user context - Migrate Composio integration routes to new auth patterns - Update rate limiting to work with Better Auth user identification - Ensure all API routes properly handle Better Auth authentication This maintains API functionality while adapting to the new Better Auth authentication system for server-side route handlers. --- app/api/chat/route.ts | 4 ++-- app/api/composio/connect/route.ts | 4 ++-- app/api/composio/disconnect/route.ts | 4 ++-- app/api/composio/status/route.ts | 4 ++-- app/api/create-chat/route.ts | 4 ++-- app/api/rate-limits/route.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 00caa73..ad2ff50 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -2,7 +2,6 @@ import type { AnthropicProviderOptions } from "@ai-sdk/anthropic"; import type { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; import type { OpenAIResponsesProviderOptions } from "@ai-sdk/openai"; -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; // import { withTracing } from '@posthog/ai'; import { consumeStream, @@ -24,6 +23,7 @@ import { searchTool } from "@/app/api/tools/search"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import type { Message } from "@/convex/schema/message"; +import { getToken } from "@/lib/auth-server"; import { MODELS_MAP } from "@/lib/config"; import { calculateConnectorStatus } from "@/lib/connector-utils"; import { createAgentTool } from "@/lib/create-agent-tool"; @@ -507,7 +507,7 @@ export async function POST(req: Request) { return createErrorResponse(new Error("Invalid 'model' provided.")); } - const token = await convexAuthNextjsToken(); + const token = await getToken(); // Get current user first (needed for multiple operations below) const user = await fetchQuery(api.users.getCurrentUser, {}, { token }); diff --git a/app/api/composio/connect/route.ts b/app/api/composio/connect/route.ts index b6380df..e8d2cf5 100644 --- a/app/api/composio/connect/route.ts +++ b/app/api/composio/connect/route.ts @@ -1,7 +1,7 @@ -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; import { fetchQuery } from "convex/nextjs"; import { NextResponse } from "next/server"; import { api } from "@/convex/_generated/api"; +import { getToken } from "@/lib/auth-server"; import { initiateConnection } from "@/lib/composio-server"; import { SUPPORTED_CONNECTORS } from "@/lib/config/tools"; import { createErrorResponse } from "@/lib/error-utils"; @@ -9,7 +9,7 @@ import type { ConnectorType } from "@/lib/types"; export async function POST(request: Request) { try { - const token = await convexAuthNextjsToken(); + const token = await getToken(); if (!token) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } diff --git a/app/api/composio/disconnect/route.ts b/app/api/composio/disconnect/route.ts index f6ab2ae..ca6ddda 100644 --- a/app/api/composio/disconnect/route.ts +++ b/app/api/composio/disconnect/route.ts @@ -1,7 +1,7 @@ -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; import { fetchMutation, fetchQuery } from "convex/nextjs"; import { NextResponse } from "next/server"; import { api } from "@/convex/_generated/api"; +import { getToken } from "@/lib/auth-server"; import { disconnectAccount } from "@/lib/composio-server"; import { SUPPORTED_CONNECTORS } from "@/lib/config/tools"; import { createErrorResponse } from "@/lib/error-utils"; @@ -9,7 +9,7 @@ import type { ConnectorType } from "@/lib/types"; export async function POST(request: Request) { try { - const token = await convexAuthNextjsToken(); + const token = await getToken(); if (!token) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } diff --git a/app/api/composio/status/route.ts b/app/api/composio/status/route.ts index 85b4f6d..a6a025e 100644 --- a/app/api/composio/status/route.ts +++ b/app/api/composio/status/route.ts @@ -1,13 +1,13 @@ -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; import { fetchQuery } from "convex/nextjs"; import { NextResponse } from "next/server"; import { api } from "@/convex/_generated/api"; +import { getToken } from "@/lib/auth-server"; import { waitForConnection } from "@/lib/composio-server"; import { createErrorResponse } from "@/lib/error-utils"; export async function GET(request: Request) { try { - const token = await convexAuthNextjsToken(); + const token = await getToken(); if (!token) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } diff --git a/app/api/create-chat/route.ts b/app/api/create-chat/route.ts index 950a14c..eb9aca4 100644 --- a/app/api/create-chat/route.ts +++ b/app/api/create-chat/route.ts @@ -1,8 +1,8 @@ -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; import { fetchMutation, fetchQuery } from "convex/nextjs"; import { PostHog } from "posthog-node"; import { z } from "zod"; import { api } from "@/convex/_generated/api"; +import { getToken } from "@/lib/auth-server"; import { createErrorResponse } from "@/lib/error-utils"; export async function POST(request: Request) { @@ -25,7 +25,7 @@ export async function POST(request: Request) { const { title, model, personaId } = parseResult.data; - const token = await convexAuthNextjsToken(); + const token = await getToken(); const user = await fetchQuery(api.users.getCurrentUser, {}, { token }); diff --git a/app/api/rate-limits/route.ts b/app/api/rate-limits/route.ts index 578afe5..0f05b7b 100644 --- a/app/api/rate-limits/route.ts +++ b/app/api/rate-limits/route.ts @@ -1,11 +1,11 @@ -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; import { fetchQuery } from "convex/nextjs"; import { api } from "@/convex/_generated/api"; +import { getToken } from "@/lib/auth-server"; import { createErrorResponse } from "@/lib/error-utils"; export async function GET() { try { - const token = await convexAuthNextjsToken(); + const token = await getToken(); // If no valid token, the user is not authenticated if (!token) { From 403f3b8c60dacad5782a34a5738ec0de859a20ba Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:14:53 +0100 Subject: [PATCH 05/10] feat: migrate backend queries and mutations to Better Auth - Use authComponent.safeGetAuthUser(ctx) for safe authentication checks - Update auth helper functions to work with Better Auth user object structure - Migrate all queries/mutations in chats, messages, files, users, api_keys - Update subscription and connector functions for new auth system - Preserve existing data relationships and user ID mappings This completes the backend migration while maintaining all existing functionality and data integrity through Better Auth triggers. --- convex/api_keys.ts | 33 +++++--- convex/chats.ts | 17 ++-- convex/connectors.ts | 13 +-- convex/files.ts | 61 ++++++++------ convex/import_export.ts | 7 +- convex/lib/auth_helper.ts | 14 ++-- convex/messages.ts | 12 +-- convex/subscription.ts | 17 ++-- convex/users.ts | 172 ++++++++++++++++++-------------------- 9 files changed, 179 insertions(+), 167 deletions(-) diff --git a/convex/api_keys.ts b/convex/api_keys.ts index ebb63cb..ef05d04 100644 --- a/convex/api_keys.ts +++ b/convex/api_keys.ts @@ -1,7 +1,8 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { ConvexError, v } from "convex/values"; import { ERROR_CODES } from "../lib/error-codes"; +import type { Id } from "./_generated/dataModel"; import { mutation, query } from "./_generated/server"; +import { authComponent } from "./auth"; const API_KEY_SECRET = process.env.API_KEY_SECRET; if (!API_KEY_SECRET) { @@ -173,11 +174,12 @@ export const getApiKeys = query({ }) ), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { // Return empty array for unauthenticated users return []; } + const userId = authUser.userId as Id<"users">; const keys = await ctx.db .query("user_api_keys") .withIndex("by_user_provider", (q) => q.eq("userId", userId)) @@ -211,10 +213,11 @@ export const saveApiKey = mutation({ }, returns: v.null(), handler: async (ctx, { provider, key, mode }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const [encrypted, existing] = await Promise.all([ encrypt(key, userId), ctx.db @@ -262,10 +265,11 @@ export const deleteApiKey = mutation({ }, returns: v.null(), handler: async (ctx, { provider }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const existing = await ctx.db .query("user_api_keys") .withIndex("by_user_provider", (q) => @@ -294,10 +298,11 @@ export const updateApiKeyMode = mutation({ }, returns: v.null(), handler: async (ctx, { provider, mode }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const existing = await ctx.db .query("user_api_keys") .withIndex("by_user_provider", (q) => @@ -324,10 +329,11 @@ export const getDecryptedKey = query({ ), }, handler: async (ctx, { provider }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const existing = await ctx.db .query("user_api_keys") .withIndex("by_user_provider", (q) => @@ -349,10 +355,11 @@ export const incrementUserApiKeyUsage = mutation({ args: { provider: v.string() }, returns: v.null(), handler: async (ctx, { provider }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const existing = await ctx.db .query("user_api_keys") .withIndex("by_user_provider", (q) => diff --git a/convex/chats.ts b/convex/chats.ts index 30ed759..6b7930f 100644 --- a/convex/chats.ts +++ b/convex/chats.ts @@ -1,9 +1,9 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { ConvexError, v } from "convex/values"; import { ERROR_CODES } from "../lib/error-codes"; import { detectRedactedContent } from "../lib/redacted-content-detector"; import type { Id } from "./_generated/dataModel"; import { internalMutation, mutation, query } from "./_generated/server"; +import { authComponent } from "./auth"; // Import helper functions import { ensureAuthenticated, @@ -92,10 +92,11 @@ export const forkFromShared = mutation({ args: { sourceChatId: v.id("chats") }, returns: v.object({ chatId: v.id("chats") }), handler: async (ctx, { sourceChatId }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const source = await ctx.db.get(sourceChatId); if (!(source && (source.public ?? false))) { @@ -201,10 +202,11 @@ export const listChatsForUser = query({ }) ), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return []; } + const userId = authUser.userId as Id<"users">; return await ctx.db .query("chats") .withIndex("by_user", (q) => q.eq("userId", userId)) @@ -273,10 +275,11 @@ export const deleteAllChatsForUser = mutation({ args: {}, returns: v.null(), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return null; } + const userId = authUser.userId as Id<"users">; const chats = await ctx.db .query("chats") diff --git a/convex/connectors.ts b/convex/connectors.ts index aa3b9e1..9a9be23 100644 --- a/convex/connectors.ts +++ b/convex/connectors.ts @@ -1,12 +1,13 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { ConvexError, v } from "convex/values"; import { ERROR_CODES } from "../lib/error-codes"; +import type { Id } from "./_generated/dataModel"; import { internalMutation, internalQuery, mutation, query, } from "./_generated/server"; +import { authComponent } from "./auth"; import { ensureAuthenticated } from "./lib/auth_helper"; // Type for connector types @@ -41,10 +42,11 @@ export const listUserConnectors = query({ }) ), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return []; } + const userId = authUser.userId as Id<"users">; const connectors = await ctx.db .query("connectors") .withIndex("by_user", (q) => q.eq("userId", userId)) @@ -75,10 +77,11 @@ export const getConnectorByType = query({ v.null() ), handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return null; } + const userId = authUser.userId as Id<"users">; const connector = await ctx.db .query("connectors") .withIndex("by_user_and_type", (q) => diff --git a/convex/files.ts b/convex/files.ts index 43532da..0806d06 100644 --- a/convex/files.ts +++ b/convex/files.ts @@ -1,4 +1,3 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { R2, type R2Callbacks } from "@convex-dev/r2"; import { ConvexError, v } from "convex/values"; import { UPLOAD_ALLOWED_MIME, UPLOAD_MAX_BYTES } from "@/lib/config/upload"; @@ -7,6 +6,7 @@ import { ERROR_CODES } from "../lib/error-codes"; import { api, components, internal } from "./_generated/api"; import type { Id } from "./_generated/dataModel"; import { action, internalMutation, mutation, query } from "./_generated/server"; +import { authComponent } from "./auth"; // R2 client and client API exports (used by React upload hook and server routes) const r2 = new R2(components.r2); @@ -19,17 +19,18 @@ export const { generateUploadUrl, syncMetadata, onSyncMetadata } = r2.clientApi( // Provide callbacks reference so the component can invoke onSyncMetadata callbacks, checkUpload: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await ctx.auth.getUserIdentity(); + if (authUser === null) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } }, // Create a pending attachment row on upload start onUpload: async (ctx, _bucket, key) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // If a row for this key already exists for this user, skip insert const existing = await ctx.db @@ -146,10 +147,11 @@ export const saveFileAttachment = action({ fileName: v.string(), }, handler: async (ctx, args): Promise => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // Ensure we have fresh metadata from R2 // await ctx.runMutation(api.files.syncMetadata, { key: args.key }); @@ -216,8 +218,8 @@ export const saveGeneratedImage = action({ fileName: v.optional(v.string()), }, handler: async (ctx, args): Promise => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await ctx.auth.getUserIdentity(); + if (authUser === null) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } @@ -270,10 +272,11 @@ export const internalSave = internalMutation({ url: v.optional(v.string()), }, handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // Verify that the key belongs to the current user via the pending row. const existing = await ctx.db @@ -343,10 +346,11 @@ export const internalSaveGenerated = internalMutation({ url: v.optional(v.string()), }, handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // Verify ownership of the key to this user const existing = await ctx.db @@ -389,10 +393,11 @@ export const internalSaveGenerated = internalMutation({ export const getAttachment = query({ args: { attachmentId: v.id("chat_attachments") }, handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const attachment = await ctx.db.get(args.attachmentId); if (!attachment || attachment.userId !== userId) { @@ -406,10 +411,11 @@ export const getAttachment = query({ export const findAttachmentByKey = query({ args: { key: v.string() }, handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const row = await ctx.db .query("chat_attachments") .withIndex("by_key", (q) => q.eq("key", args.key)) @@ -428,8 +434,8 @@ export const getStorageUrl = query({ args: { key: v.string() }, returns: v.union(v.string(), v.null()), handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await ctx.auth.getUserIdentity(); + if (authUser === null) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } @@ -452,10 +458,11 @@ export const getStorageUrl = query({ export const getAttachmentsForUser = query({ args: {}, handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { - throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { + return []; } + const userId = authUser.userId as Id<"users">; const attachments = await ctx.db .query("chat_attachments") @@ -475,10 +482,11 @@ export const getAttachmentsForUser = query({ export const deleteAttachments = mutation({ args: { attachmentIds: v.array(v.id("chat_attachments")) }, handler: async (ctx, { attachmentIds }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // Create a Set for O(1) lookup of attachment IDs to delete const attachmentIdsToDelete = new Set(attachmentIds); @@ -508,10 +516,11 @@ export const deleteAttachments = mutation({ export const getAttachmentsForChat = query({ args: { chatId: v.id("chats") }, handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // Verify ownership of the chat const chat = await ctx.db.get(args.chatId); diff --git a/convex/import_export.ts b/convex/import_export.ts index 16696f0..b911a8e 100644 --- a/convex/import_export.ts +++ b/convex/import_export.ts @@ -1,8 +1,8 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { ConvexError, v } from "convex/values"; import { ERROR_CODES } from "../lib/error-codes"; import type { Id } from "./_generated/dataModel"; import { mutation } from "./_generated/server"; +import { authComponent } from "./auth"; export const bulkImportChat = mutation({ args: { @@ -44,10 +44,11 @@ export const bulkImportChat = mutation({ messageCount: v.number(), }), handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // Create chat const now = Date.now(); diff --git a/convex/lib/auth_helper.ts b/convex/lib/auth_helper.ts index 01db82f..66ebcaa 100644 --- a/convex/lib/auth_helper.ts +++ b/convex/lib/auth_helper.ts @@ -1,8 +1,8 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { ConvexError } from "convex/values"; import { ERROR_CODES } from "../../lib/error-codes"; import type { Doc, Id } from "../_generated/dataModel"; import type { MutationCtx, QueryCtx } from "../_generated/server"; +import { authComponent } from "../auth"; /** * Ensures the user is authenticated and returns their userId. @@ -14,11 +14,11 @@ import type { MutationCtx, QueryCtx } from "../_generated/server"; export async function ensureAuthenticated( ctx: QueryCtx | MutationCtx ): Promise> { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } - return userId; + return authUser.userId as Id<"users">; } /** @@ -107,11 +107,11 @@ export async function ensureMessageAccess( export async function getCurrentUserOrNull( ctx: QueryCtx | MutationCtx ): Promise | null> { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return null; } - return await ctx.db.get(userId); + return await ctx.db.get(authUser.userId as Id<"users">); } /** diff --git a/convex/messages.ts b/convex/messages.ts index 0976d98..3aec16f 100644 --- a/convex/messages.ts +++ b/convex/messages.ts @@ -1,4 +1,3 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { R2 } from "@convex-dev/r2"; import { v } from "convex/values"; import { components } from "./_generated/api"; @@ -9,6 +8,7 @@ import { mutation, query, } from "./_generated/server"; +import { authComponent } from "./auth"; // Import helper functions import { ensureChatAccess, ensureMessageAccess } from "./lib/auth_helper"; import { sanitizeMessageParts } from "./lib/sanitization_helper"; @@ -389,10 +389,11 @@ export const deleteMessageAndDescendants = mutation({ returns: v.object({ chatDeleted: v.boolean() }), handler: async (ctx, { messageId, deleteOnlyDescendants = false }) => { // Try to get message access - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return { chatDeleted: false }; } + const userId = authUser.userId as Id<"users">; const message = await ctx.db.get(messageId); if (!message) { return { chatDeleted: false }; @@ -526,10 +527,11 @@ export const searchMessages = query({ ), handler: async (ctx, { query: search, limit = 20 }) => { const safeLimit = Math.min(Math.max(1, limit), 100); // Cap between 1-100 - const userId = await getAuthUserId(ctx); - if (!userId || search.trim() === "") { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId || search.trim() === "") { return []; } + const userId = authUser.userId as Id<"users">; const results = await ctx.db .query("messages") diff --git a/convex/subscription.ts b/convex/subscription.ts index c534809..6565bd6 100644 --- a/convex/subscription.ts +++ b/convex/subscription.ts @@ -16,7 +16,8 @@ export const onSubscriptionUpdated = internalMutation({ if (directUserId) { const user = await ctx.db.get(directUserId as Id<"users">); if (user) { - await resetUserRateLimits(ctx, user._id, user.isAnonymous ?? false); + // Subscription users are always authenticated + await resetUserRateLimits(ctx, user._id); return null; } } @@ -28,18 +29,12 @@ export const onSubscriptionUpdated = internalMutation({ /** * Helper function to reset all rate limits for a user */ -async function resetUserRateLimits( - ctx: MutationCtx, - userId: Id<"users">, - isAnonymous: boolean -) { - // Reset daily limits (for non-premium tracking) - const dailyLimitName = isAnonymous ? "anonymousDaily" : "authenticatedDaily"; - +async function resetUserRateLimits(ctx: MutationCtx, userId: Id<"users">) { + // Reset daily limits (subscription users are always authenticated) // Run all rate limiter operations concurrently const operations = [ - rateLimiter.reset(ctx, dailyLimitName, { key: userId }), - rateLimiter.limit(ctx, dailyLimitName, { key: userId, count: 0 }), + rateLimiter.reset(ctx, "authenticatedDaily", { key: userId }), + rateLimiter.limit(ctx, "authenticatedDaily", { key: userId, count: 0 }), rateLimiter.reset(ctx, "standardMonthly", { key: userId }), rateLimiter.limit(ctx, "standardMonthly", { key: userId, count: 0 }), rateLimiter.reset(ctx, "premiumMonthly", { key: userId }), diff --git a/convex/users.ts b/convex/users.ts index f4db758..2fc0b85 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -1,4 +1,3 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; import { R2 } from "@convex-dev/r2"; import { calculateRateLimit, @@ -15,6 +14,7 @@ import { mutation, query, } from "./_generated/server"; +import { authComponent } from "./auth"; import { RATE_LIMITS } from "./lib/rateLimitConstants"; import { polar } from "./polar"; import { rateLimiter } from "./rateLimiter"; @@ -31,11 +31,28 @@ export const getCurrentUser = query({ }) ), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return null; } - return await ctx.db.get(userId); + + const customUser = await ctx.db.get(authUser.userId as Id<"users">); + if (!customUser) { + return null; + } + + // Merge Better Auth user data with custom user data + // Better Auth fields override custom table fields + return { + ...customUser, + _id: authUser.userId as Id<"users">, + // Use Better Auth fields instead of custom table duplicates + name: authUser.name, + email: authUser.email, + image: authUser.image ?? undefined, + isAnonymous: authUser.isAnonymous ?? undefined, + // Note: Better Auth uses emailVerified (boolean), not emailVerificationTime (number) + }; }, }); @@ -43,13 +60,15 @@ export const userHasPremium = query({ args: {}, returns: v.boolean(), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { return false; } try { - const subscription = await polar.getCurrentSubscription(ctx, { userId }); + const subscription = await polar.getCurrentSubscription(ctx, { + userId: authUser.userId as Id<"users">, + }); return subscription?.status === "active"; } catch { return false; @@ -78,17 +97,17 @@ export const updateUserProfile = mutation({ }, returns: v.null(), handler: async (ctx, { updates }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { - return null; + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { + throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } - const user = await ctx.db.get(userId); + const user = await ctx.db.get(authUser.userId as Id<"users">); if (!user) { return null; } // Apply the updates - await ctx.db.patch(userId, { ...updates }); + await ctx.db.patch(authUser.userId as Id<"users">, { ...updates }); return null; }, @@ -98,12 +117,12 @@ export const toggleFavoriteModel = mutation({ args: { modelId: v.string() }, returns: v.null(), handler: async (ctx, { modelId }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } - const user = await ctx.db.get(userId); + const user = await ctx.db.get(authUser.userId as Id<"users">); if (!user) { throw new ConvexError(ERROR_CODES.USER_NOT_FOUND); } @@ -128,7 +147,7 @@ export const toggleFavoriteModel = mutation({ newDisabled = currentDisabled.filter((id) => id !== modelId); } - await ctx.db.patch(userId, { + await ctx.db.patch(user._id, { favoriteModels: newFavorites, disabledModels: newDisabled, }); @@ -141,10 +160,11 @@ export const setModelEnabled = mutation({ args: { modelId: v.string(), enabled: v.boolean() }, returns: v.null(), handler: async (ctx, { modelId, enabled }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const user = await ctx.db.get(userId); if (!user) { @@ -195,10 +215,11 @@ export const bulkSetModelsDisabled = mutation({ args: { modelIds: v.array(v.string()) }, returns: v.null(), handler: async (ctx, { modelIds }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const user = await ctx.db.get(userId); if (!user) { @@ -248,10 +269,11 @@ export const bulkSetFavoriteModels = mutation({ args: { modelIds: v.array(v.string()) }, returns: v.null(), handler: async (ctx, { modelIds }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const user = await ctx.db.get(userId); if (!user) { @@ -288,21 +310,24 @@ export const incrementMessageCount = mutation({ }, returns: v.null(), handler: async (ctx, { usesPremiumCredits }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const user = await ctx.db.get(userId); if (!user) { throw new ConvexError(ERROR_CODES.USER_NOT_FOUND); } + // Use the authUser we already fetched to check isAnonymous status + const isAnonymous = authUser?.isAnonymous ?? false; + const subscription = await polar.getCurrentSubscription(ctx, { userId: user._id, }); const isPremium = subscription?.status === "active"; - const isAnonymous = user.isAnonymous ?? false; // For premium users using premium models, deduct from premium credits if (isPremium && usesPremiumCredits) { @@ -340,21 +365,24 @@ export const assertNotOverLimit = mutation({ usesPremiumCredits: v.optional(v.boolean()), }, handler: async (ctx, { usesPremiumCredits }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; const user = await ctx.db.get(userId); if (!user) { throw new ConvexError(ERROR_CODES.USER_NOT_FOUND); } + // Use the authUser we already fetched to check isAnonymous status + const isAnonymous = authUser?.isAnonymous ?? false; + const subscription = await polar.getCurrentSubscription(ctx, { userId: user._id, }); const isPremium = subscription?.status === "active"; - const isAnonymous = user.isAnonymous ?? false; // For premium users using premium models, check premium credits if (isPremium && usesPremiumCredits) { @@ -426,9 +454,8 @@ export const getRateLimitStatus = query({ premiumReset: v.optional(v.number()), }), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - // console.log('getRateLimitStatus called for userId:', userId); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { // Return safe defaults for unauthenticated users return { isPremium: false, @@ -445,16 +472,18 @@ export const getRateLimitStatus = query({ }; } - const user = await ctx.db.get(userId); + const user = await ctx.db.get(authUser.userId as Id<"users">); if (!user) { throw new ConvexError(ERROR_CODES.USER_NOT_FOUND); } + // Use the authUser we already fetched to check isAnonymous status + const isAnonymous = authUser?.isAnonymous ?? false; + const subscription = await polar.getCurrentSubscription(ctx, { userId: user._id, }); const isPremium = subscription?.status === "active"; - const isAnonymous = user.isAnonymous ?? false; const now = Date.now(); // Determine daily limit name for non-premium users @@ -473,17 +502,17 @@ export const getRateLimitStatus = query({ ] = await Promise.all([ isPremium ? Promise.resolve(null) - : rateLimiter.check(ctx, dailyLimitName, { key: userId }), + : rateLimiter.check(ctx, dailyLimitName, { key: authUser.userId }), isPremium ? Promise.resolve(null) - : rateLimiter.getValue(ctx, dailyLimitName, { key: userId }), - rateLimiter.check(ctx, "standardMonthly", { key: userId }), - rateLimiter.getValue(ctx, "standardMonthly", { key: userId }), + : rateLimiter.getValue(ctx, dailyLimitName, { key: authUser.userId }), + rateLimiter.check(ctx, "standardMonthly", { key: authUser.userId }), + rateLimiter.getValue(ctx, "standardMonthly", { key: authUser.userId }), isPremium - ? rateLimiter.check(ctx, "premiumMonthly", { key: userId }) + ? rateLimiter.check(ctx, "premiumMonthly", { key: authUser.userId }) : Promise.resolve(null), isPremium - ? rateLimiter.getValue(ctx, "premiumMonthly", { key: userId }) + ? rateLimiter.getValue(ctx, "premiumMonthly", { key: authUser.userId }) : Promise.resolve(null), ]); @@ -595,21 +624,14 @@ export const deleteAccount = mutation({ args: {}, returns: v.null(), handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) { + const authUser = await authComponent.safeGetAuthUser(ctx); + if (!authUser?.userId) { throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); } + const userId = authUser.userId as Id<"users">; // --- Step 1: Fetch all documents that need to be deleted in parallel --- - const [ - attachments, - messages, - chats, - feedback, - usage, - authAccounts, - authSessions, - ] = await Promise.all([ + const [attachments, messages, chats, usage] = await Promise.all([ ctx.db .query("chat_attachments") .withIndex("by_userId", (q) => q.eq("userId", userId)) @@ -622,22 +644,10 @@ export const deleteAccount = mutation({ .query("chats") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(), - ctx.db - .query("feedback") - .withIndex("by_user", (q) => q.eq("userId", userId)) - .collect(), ctx.db .query("usage_history") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(), - ctx.db - .query("authAccounts") - .withIndex("userIdAndProvider", (q) => q.eq("userId", userId)) - .collect(), - ctx.db - .query("authSessions") - .withIndex("userId", (q) => q.eq("userId", userId)) - .collect(), ]); // --- Step 2: Collect all deletion promises and execute them concurrently --- @@ -664,23 +674,11 @@ export const deleteAccount = mutation({ // Delete chats deletionPromises.push(...chats.map((chat) => ctx.db.delete(chat._id))); - // Delete feedback - deletionPromises.push(...feedback.map((f) => ctx.db.delete(f._id))); - // Delete usage history deletionPromises.push(...usage.map((u) => ctx.db.delete(u._id))); - // Delete auth accounts - deletionPromises.push( - ...authAccounts.map((acc) => ctx.db.delete(acc._id as Id<"authAccounts">)) - ); - - // Delete auth sessions - deletionPromises.push( - ...authSessions.map((sess) => - ctx.db.delete(sess._id as Id<"authSessions">) - ) - ); + // Note: Better Auth manages its own user deletion through the onDelete trigger + // in convex/auth.ts, so we don't need to manually delete auth tables // Execute all deletions concurrently await Promise.allSettled(deletionPromises); @@ -691,18 +689,6 @@ export const deleteAccount = mutation({ }, }); -// React hook API functions for rate limiting -export const { getRateLimit: getRateLimitHook, getServerTime } = - rateLimiter.hookAPI( - "authenticatedDaily", // Default rate limit - { - key: async (ctx) => { - const userId = await getAuthUserId(ctx); - return userId || "anonymous"; - }, - } - ); - // Internal query to get user by ID export const getUser = internalQuery({ args: { userId: v.id("users") }, @@ -731,11 +717,14 @@ export const assertNotOverLimitInternal = internalMutation({ throw new ConvexError(ERROR_CODES.USER_NOT_FOUND); } + // Internal functions are only called for authenticated users (scheduled tasks require auth) + // Anonymous users cannot create scheduled tasks, so isAnonymous = false + const isAnonymous = false; + const subscription = await polar.getCurrentSubscription(ctx, { userId: user._id, }); const isPremium = subscription?.status === "active"; - const isAnonymous = user.isAnonymous ?? false; // For premium users using premium models, check premium credits if (isPremium && usesPremiumCredits) { @@ -799,11 +788,14 @@ export const incrementMessageCountInternal = internalMutation({ throw new ConvexError(ERROR_CODES.USER_NOT_FOUND); } + // Internal functions are only called for authenticated users (scheduled tasks require auth) + // Anonymous users cannot create scheduled tasks, so isAnonymous = false + const isAnonymous = false; + const subscription = await polar.getCurrentSubscription(ctx, { userId: user._id, }); const isPremium = subscription?.status === "active"; - const isAnonymous = user.isAnonymous ?? false; // For premium users using premium models, deduct from premium credits if (isPremium && usesPremiumCredits) { From a00dfb55fbaa3a639d6c4a41f504f97fd7575446 Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:15:42 +0100 Subject: [PATCH 06/10] feat: finalize Better Auth migration with schema and cleanup - Update Convex schema to work with Better Auth user structure - Remove deprecated feedback functionality and middleware - Update hooks and pages to use new auth patterns - Regenerate Convex API types for Better Auth integration - Update HTTP handlers and instrumentation for new auth system - Update README documentation for Better Auth setup This completes the migration from Convex Auth to Better Auth, removing deprecated code and ensuring consistency across the application. --- README.md | 14 +- app/c/[chatId]/page.tsx | 4 +- app/hooks/use-model-preferences.ts | 4 +- app/hooks/use-model-settings.ts | 14 +- app/privacy/page.tsx | 2 +- app/security/page.tsx | 2 +- convex/_generated/api.d.ts | 5277 +++++++++++++++++++++++++++- convex/feedback.ts | 21 - convex/http.ts | 5 +- convex/schema.ts | 4 - convex/schema/feedback.ts | 7 - convex/schema/user.ts | 8 +- instrumentation-client.ts | 2 +- middleware.ts | 7 - 14 files changed, 5304 insertions(+), 67 deletions(-) delete mode 100644 convex/feedback.ts delete mode 100644 convex/schema/feedback.ts delete mode 100644 middleware.ts diff --git a/README.md b/README.md index d93f090..e53d981 100644 --- a/README.md +++ b/README.md @@ -195,13 +195,13 @@ Configure the required and optional Convex environment variables for your applic #### A. Authentication (Required) -OS Chat uses Convex Auth for authentication with Google OAuth. +OS Chat uses Better Auth for authentication with Google OAuth. -1. **Initialize Convex Auth:** +1. **Configure Better Auth:** ```bash - # Initialize Convex Auth setup - bunx @convex-dev/auth + # Better Auth is already configured in the codebase + # Set up your Google OAuth credentials in the Convex dashboard ``` 2. **Set up Google OAuth:** @@ -274,7 +274,7 @@ bunx convex env set POLAR_WEBHOOK_SECRET your-polar-webhook-secret **Reference Documentation:** -- [Convex Auth Setup Guide](https://labs.convex.dev/auth/setup) +- [Convex Better Auth Documentation](https://convex-better-auth.netlify.app/) - [Google OAuth Configuration](https://labs.convex.dev/auth/config/oauth/google) - [Cloudflare R2 Component](https://www.convex.dev/components/cloudflare-r2) - [Polar Component Documentation](https://www.convex.dev/components/polar) @@ -336,7 +336,7 @@ For production deployment: - Verify OAuth credentials in Convex dashboard - Check `SITE_URL` matches your development/production URL -- Ensure Convex Auth is properly configured +- Ensure Better Auth is properly configured **API Key Issues**: @@ -353,7 +353,7 @@ For production deployment: **Need Help?** - Check the [Convex Documentation](https://docs.convex.dev) -- Review the [Convex Auth Setup Guide](https://labs.convex.dev/auth/setup) +- Review the [Convex Better Auth Documentation](https://convex-better-auth.netlify.app/) - See [Google OAuth Configuration](https://labs.convex.dev/auth/config/oauth/google) for authentication - Configure [Cloudflare R2](https://www.convex.dev/components/cloudflare-r2) for file storage - Get an [Exa API key](https://exa.ai/) for web search functionality diff --git a/app/c/[chatId]/page.tsx b/app/c/[chatId]/page.tsx index 93bf722..7a3168a 100644 --- a/app/c/[chatId]/page.tsx +++ b/app/c/[chatId]/page.tsx @@ -1,8 +1,8 @@ -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; import { fetchQuery } from "convex/nextjs"; import { redirect } from "next/navigation"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; +import { getToken } from "@/lib/auth-server"; import Chat from "../../components/chat/chat"; @@ -12,7 +12,7 @@ export default async function ChatPage({ params: Promise<{ chatId: string }>; }) { // Validate the chat on the server to avoid client-side flashes. - const token = await convexAuthNextjsToken(); + const token = await getToken(); // If we fail to obtain a token (anonymous visitor with cookies disabled, etc.) // we still attempt the query – Convex will treat it as anonymous and only diff --git a/app/hooks/use-model-preferences.ts b/app/hooks/use-model-preferences.ts index 981403e..ddca698 100644 --- a/app/hooks/use-model-preferences.ts +++ b/app/hooks/use-model-preferences.ts @@ -24,7 +24,7 @@ export function useModelPreferences() { } const newFavorites = isFavorite - ? favorites.filter((id) => id !== modelId) + ? favorites.filter((id: string) => id !== modelId) : [...favorites, modelId]; localStore.setQuery( @@ -50,7 +50,7 @@ export function useModelPreferences() { // Remove favorite models from disabled list (auto-enable favorites) const newDisabled = currentDisabled.filter( - (id) => !newFavorites.includes(id) + (id: string) => !newFavorites.includes(id) ); localStore.setQuery( diff --git a/app/hooks/use-model-settings.ts b/app/hooks/use-model-settings.ts index 6b01475..0c51503 100644 --- a/app/hooks/use-model-settings.ts +++ b/app/hooks/use-model-settings.ts @@ -27,7 +27,7 @@ export function useModelSettings() { if (enabled) { // Enabling model - remove from disabled list - newDisabled = currentDisabled.filter((id) => id !== modelId); + newDisabled = currentDisabled.filter((id: string) => id !== modelId); } else { // Disabling model - add to disabled list and remove from favorites if (modelId === MODEL_DEFAULT) { @@ -37,13 +37,13 @@ export function useModelSettings() { newDisabled = currentDisabled.includes(modelId) ? currentDisabled : [...currentDisabled, modelId]; - newFavorites = currentFavorites.filter((id) => id !== modelId); + newFavorites = currentFavorites.filter((id: string) => id !== modelId); // Ensure at least one favorite remains if (newFavorites.length === 0 && currentFavorites.length > 0) { const firstFavorite = currentFavorites[0]; newFavorites = [firstFavorite]; - newDisabled = newDisabled.filter((id) => id !== firstFavorite); + newDisabled = newDisabled.filter((id: string) => id !== firstFavorite); } } @@ -70,11 +70,13 @@ export function useModelSettings() { const currentDisabled = currentUser.disabledModels ?? []; // Filter out MODEL_DEFAULT from the models to disable - const modelsToDisable = modelIds.filter((id) => id !== MODEL_DEFAULT); + const modelsToDisable = modelIds.filter( + (id: string) => id !== MODEL_DEFAULT + ); // Remove disabled models from favorites let newFavorites = currentFavorites.filter( - (id) => !modelsToDisable.includes(id) + (id: string) => !modelsToDisable.includes(id) ); // Merge with existing disabled models let newDisabled = [...new Set([...currentDisabled, ...modelsToDisable])]; @@ -83,7 +85,7 @@ export function useModelSettings() { if (newFavorites.length === 0 && currentFavorites.length > 0) { const firstFavorite = currentFavorites[0]; newFavorites = [firstFavorite]; - newDisabled = newDisabled.filter((id) => id !== firstFavorite); + newDisabled = newDisabled.filter((id: string) => id !== firstFavorite); } localStore.setQuery( diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx index a09efb0..4c915dd 100644 --- a/app/privacy/page.tsx +++ b/app/privacy/page.tsx @@ -127,7 +127,7 @@ export default function PrivacyPage() { responses
  • - Authentication: Convex Auth for secure login + Authentication: Better Auth for secure login
  • Payments: Polar for subscription billing diff --git a/app/security/page.tsx b/app/security/page.tsx index 6855d5c..94570c2 100644 --- a/app/security/page.tsx +++ b/app/security/page.tsx @@ -48,7 +48,7 @@ export default function SecurityPage() { Access Controls:

      -
    • Authentication via Google OAuth through Convex Auth
    • +
    • Authentication via Google OAuth through Better Auth
    • Users can only access their own data
    • Server-side validation for all data access requests
    • diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 235b92a..3f80b12 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -13,7 +13,6 @@ import type * as auth from "../auth.js"; import type * as chats from "../chats.js"; import type * as connectors from "../connectors.js"; import type * as email from "../email.js"; -import type * as feedback from "../feedback.js"; import type * as files from "../files.js"; import type * as http from "../http.js"; import type * as import_export from "../import_export.js"; @@ -22,6 +21,8 @@ import type * as lib_cleanup_helper from "../lib/cleanup_helper.js"; import type * as lib_rateLimitConstants from "../lib/rateLimitConstants.js"; import type * as lib_sanitization_helper from "../lib/sanitization_helper.js"; import type * as messages from "../messages.js"; +import type * as migrations_cleanUserData from "../migrations/cleanUserData.js"; +import type * as migrations_removeDuplicateUserFields from "../migrations/removeDuplicateUserFields.js"; import type * as polar from "../polar.js"; import type * as rateLimiter from "../rateLimiter.js"; import type * as scheduled_ai from "../scheduled_ai.js"; @@ -29,7 +30,6 @@ import type * as scheduled_tasks from "../scheduled_tasks.js"; import type * as schema_chat from "../schema/chat.js"; import type * as schema_chat_attachment from "../schema/chat_attachment.js"; import type * as schema_connectors from "../schema/connectors.js"; -import type * as schema_feedback from "../schema/feedback.js"; import type * as schema_message from "../schema/message.js"; import type * as schema_scheduled_task from "../schema/scheduled_task.js"; import type * as schema_task_history from "../schema/task_history.js"; @@ -60,7 +60,6 @@ declare const fullApi: ApiFromModules<{ chats: typeof chats; connectors: typeof connectors; email: typeof email; - feedback: typeof feedback; files: typeof files; http: typeof http; import_export: typeof import_export; @@ -69,6 +68,8 @@ declare const fullApi: ApiFromModules<{ "lib/rateLimitConstants": typeof lib_rateLimitConstants; "lib/sanitization_helper": typeof lib_sanitization_helper; messages: typeof messages; + "migrations/cleanUserData": typeof migrations_cleanUserData; + "migrations/removeDuplicateUserFields": typeof migrations_removeDuplicateUserFields; polar: typeof polar; rateLimiter: typeof rateLimiter; scheduled_ai: typeof scheduled_ai; @@ -76,7 +77,6 @@ declare const fullApi: ApiFromModules<{ "schema/chat": typeof schema_chat; "schema/chat_attachment": typeof schema_chat_attachment; "schema/connectors": typeof schema_connectors; - "schema/feedback": typeof schema_feedback; "schema/message": typeof schema_message; "schema/scheduled_task": typeof schema_scheduled_task; "schema/task_history": typeof schema_task_history; @@ -1008,4 +1008,5273 @@ export declare const components: { >; }; }; + betterAuth: { + adapter: { + create: FunctionReference< + "mutation", + "internal", + { + input: + | { + data: { + createdAt: number; + displayUsername?: null | string; + email: string; + emailVerified: boolean; + image?: null | string; + isAnonymous?: null | boolean; + name: string; + phoneNumber?: null | string; + phoneNumberVerified?: null | boolean; + twoFactorEnabled?: null | boolean; + updatedAt: number; + userId?: null | string; + username?: null | string; + }; + model: "user"; + } + | { + data: { + createdAt: number; + expiresAt: number; + ipAddress?: null | string; + token: string; + updatedAt: number; + userAgent?: null | string; + userId: string; + }; + model: "session"; + } + | { + data: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + accountId: string; + createdAt: number; + idToken?: null | string; + password?: null | string; + providerId: string; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scope?: null | string; + updatedAt: number; + userId: string; + }; + model: "account"; + } + | { + data: { + createdAt: number; + expiresAt: number; + identifier: string; + updatedAt: number; + value: string; + }; + model: "verification"; + } + | { + data: { backupCodes: string; secret: string; userId: string }; + model: "twoFactor"; + } + | { + data: { + aaguid?: null | string; + backedUp: boolean; + counter: number; + createdAt?: null | number; + credentialID: string; + deviceType: string; + name?: null | string; + publicKey: string; + transports?: null | string; + userId: string; + }; + model: "passkey"; + } + | { + data: { + clientId?: null | string; + clientSecret?: null | string; + createdAt?: null | number; + disabled?: null | boolean; + icon?: null | string; + metadata?: null | string; + name?: null | string; + redirectURLs?: null | string; + type?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + model: "oauthApplication"; + } + | { + data: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + clientId?: null | string; + createdAt?: null | number; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + model: "oauthAccessToken"; + } + | { + data: { + clientId?: null | string; + consentGiven?: null | boolean; + createdAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + model: "oauthConsent"; + } + | { + data: { + createdAt: number; + name: string; + organizationId: string; + updatedAt?: null | number; + }; + model: "team"; + } + | { + data: { + createdAt?: null | number; + teamId: string; + userId: string; + }; + model: "teamMember"; + } + | { + data: { + createdAt: number; + logo?: null | string; + metadata?: null | string; + name: string; + slug?: null | string; + }; + model: "organization"; + } + | { + data: { + createdAt: number; + organizationId: string; + role: string; + userId: string; + }; + model: "member"; + } + | { + data: { + email: string; + expiresAt: number; + inviterId: string; + organizationId: string; + role?: null | string; + status: string; + teamId?: null | string; + }; + model: "invitation"; + } + | { + data: { + domain: string; + issuer: string; + oidcConfig?: null | string; + organizationId?: null | string; + providerId: string; + samlConfig?: null | string; + userId?: null | string; + }; + model: "ssoProvider"; + } + | { + data: { + createdAt: number; + privateKey: string; + publicKey: string; + }; + model: "jwks"; + } + | { + data: { + cancelAtPeriodEnd?: null | boolean; + periodEnd?: null | number; + periodStart?: null | number; + plan: string; + referenceId: string; + seats?: null | number; + status?: null | string; + stripeCustomerId?: null | string; + stripeSubscriptionId?: null | string; + trialEnd?: null | number; + trialStart?: null | number; + }; + model: "subscription"; + } + | { + data: { + address: string; + chainId: number; + createdAt: number; + isPrimary?: null | boolean; + userId: string; + }; + model: "walletAddress"; + } + | { + data: { + count?: null | number; + key?: null | string; + lastRequest?: null | number; + }; + model: "rateLimit"; + } + | { + data: { count: number; key: string; lastRequest: number }; + model: "ratelimit"; + }; + onCreateHandle?: string; + select?: Array; + }, + any + >; + deleteMany: FunctionReference< + "mutation", + "internal", + { + input: + | { + model: "user"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "email" + | "emailVerified" + | "image" + | "createdAt" + | "updatedAt" + | "twoFactorEnabled" + | "isAnonymous" + | "username" + | "displayUsername" + | "phoneNumber" + | "phoneNumberVerified" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "session"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "expiresAt" + | "token" + | "createdAt" + | "updatedAt" + | "ipAddress" + | "userAgent" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "account"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accountId" + | "providerId" + | "userId" + | "accessToken" + | "refreshToken" + | "idToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "scope" + | "password" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "verification"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "identifier" + | "value" + | "expiresAt" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "twoFactor"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "secret" | "backupCodes" | "userId" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "passkey"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "publicKey" + | "userId" + | "credentialID" + | "counter" + | "deviceType" + | "backedUp" + | "transports" + | "createdAt" + | "aaguid" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthApplication"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "icon" + | "metadata" + | "clientId" + | "clientSecret" + | "redirectURLs" + | "type" + | "disabled" + | "userId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthAccessToken"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accessToken" + | "refreshToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthConsent"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "consentGiven" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "team"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "organizationId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "teamMember"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "teamId" | "userId" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "organization"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "slug" + | "logo" + | "createdAt" + | "metadata" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "member"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "userId" + | "role" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "invitation"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "email" + | "role" + | "teamId" + | "status" + | "expiresAt" + | "inviterId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ssoProvider"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "issuer" + | "oidcConfig" + | "samlConfig" + | "userId" + | "providerId" + | "organizationId" + | "domain" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "jwks"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "publicKey" | "privateKey" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "subscription"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "plan" + | "referenceId" + | "stripeCustomerId" + | "stripeSubscriptionId" + | "status" + | "periodStart" + | "periodEnd" + | "trialStart" + | "trialEnd" + | "cancelAtPeriodEnd" + | "seats" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "walletAddress"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "userId" + | "address" + | "chainId" + | "isPrimary" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "rateLimit"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ratelimit"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }; + onDeleteHandle?: string; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + }, + any + >; + deleteOne: FunctionReference< + "mutation", + "internal", + { + input: + | { + model: "user"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "email" + | "emailVerified" + | "image" + | "createdAt" + | "updatedAt" + | "twoFactorEnabled" + | "isAnonymous" + | "username" + | "displayUsername" + | "phoneNumber" + | "phoneNumberVerified" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "session"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "expiresAt" + | "token" + | "createdAt" + | "updatedAt" + | "ipAddress" + | "userAgent" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "account"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accountId" + | "providerId" + | "userId" + | "accessToken" + | "refreshToken" + | "idToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "scope" + | "password" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "verification"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "identifier" + | "value" + | "expiresAt" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "twoFactor"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "secret" | "backupCodes" | "userId" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "passkey"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "publicKey" + | "userId" + | "credentialID" + | "counter" + | "deviceType" + | "backedUp" + | "transports" + | "createdAt" + | "aaguid" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthApplication"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "icon" + | "metadata" + | "clientId" + | "clientSecret" + | "redirectURLs" + | "type" + | "disabled" + | "userId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthAccessToken"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accessToken" + | "refreshToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthConsent"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "consentGiven" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "team"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "organizationId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "teamMember"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "teamId" | "userId" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "organization"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "slug" + | "logo" + | "createdAt" + | "metadata" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "member"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "userId" + | "role" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "invitation"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "email" + | "role" + | "teamId" + | "status" + | "expiresAt" + | "inviterId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ssoProvider"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "issuer" + | "oidcConfig" + | "samlConfig" + | "userId" + | "providerId" + | "organizationId" + | "domain" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "jwks"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "publicKey" | "privateKey" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "subscription"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "plan" + | "referenceId" + | "stripeCustomerId" + | "stripeSubscriptionId" + | "status" + | "periodStart" + | "periodEnd" + | "trialStart" + | "trialEnd" + | "cancelAtPeriodEnd" + | "seats" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "walletAddress"; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "userId" + | "address" + | "chainId" + | "isPrimary" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "rateLimit"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ratelimit"; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }; + onDeleteHandle?: string; + }, + any + >; + findMany: FunctionReference< + "query", + "internal", + { + limit?: number; + model: + | "user" + | "session" + | "account" + | "verification" + | "twoFactor" + | "passkey" + | "oauthApplication" + | "oauthAccessToken" + | "oauthConsent" + | "team" + | "teamMember" + | "organization" + | "member" + | "invitation" + | "ssoProvider" + | "jwks" + | "subscription" + | "walletAddress" + | "rateLimit" + | "ratelimit"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + sortBy?: { direction: "asc" | "desc"; field: string }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }, + any + >; + findOne: FunctionReference< + "query", + "internal", + { + model: + | "user" + | "session" + | "account" + | "verification" + | "twoFactor" + | "passkey" + | "oauthApplication" + | "oauthAccessToken" + | "oauthConsent" + | "team" + | "teamMember" + | "organization" + | "member" + | "invitation" + | "ssoProvider" + | "jwks" + | "subscription" + | "walletAddress" + | "rateLimit" + | "ratelimit"; + select?: Array; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }, + any + >; + updateMany: FunctionReference< + "mutation", + "internal", + { + input: + | { + model: "user"; + update: { + createdAt?: number; + displayUsername?: null | string; + email?: string; + emailVerified?: boolean; + image?: null | string; + isAnonymous?: null | boolean; + name?: string; + phoneNumber?: null | string; + phoneNumberVerified?: null | boolean; + twoFactorEnabled?: null | boolean; + updatedAt?: number; + userId?: null | string; + username?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "email" + | "emailVerified" + | "image" + | "createdAt" + | "updatedAt" + | "twoFactorEnabled" + | "isAnonymous" + | "username" + | "displayUsername" + | "phoneNumber" + | "phoneNumberVerified" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "session"; + update: { + createdAt?: number; + expiresAt?: number; + ipAddress?: null | string; + token?: string; + updatedAt?: number; + userAgent?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "expiresAt" + | "token" + | "createdAt" + | "updatedAt" + | "ipAddress" + | "userAgent" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "account"; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + accountId?: string; + createdAt?: number; + idToken?: null | string; + password?: null | string; + providerId?: string; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scope?: null | string; + updatedAt?: number; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accountId" + | "providerId" + | "userId" + | "accessToken" + | "refreshToken" + | "idToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "scope" + | "password" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "verification"; + update: { + createdAt?: number; + expiresAt?: number; + identifier?: string; + updatedAt?: number; + value?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "identifier" + | "value" + | "expiresAt" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "twoFactor"; + update: { + backupCodes?: string; + secret?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "secret" | "backupCodes" | "userId" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "passkey"; + update: { + aaguid?: null | string; + backedUp?: boolean; + counter?: number; + createdAt?: null | number; + credentialID?: string; + deviceType?: string; + name?: null | string; + publicKey?: string; + transports?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "publicKey" + | "userId" + | "credentialID" + | "counter" + | "deviceType" + | "backedUp" + | "transports" + | "createdAt" + | "aaguid" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthApplication"; + update: { + clientId?: null | string; + clientSecret?: null | string; + createdAt?: null | number; + disabled?: null | boolean; + icon?: null | string; + metadata?: null | string; + name?: null | string; + redirectURLs?: null | string; + type?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "icon" + | "metadata" + | "clientId" + | "clientSecret" + | "redirectURLs" + | "type" + | "disabled" + | "userId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthAccessToken"; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + clientId?: null | string; + createdAt?: null | number; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accessToken" + | "refreshToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthConsent"; + update: { + clientId?: null | string; + consentGiven?: null | boolean; + createdAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "consentGiven" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "team"; + update: { + createdAt?: number; + name?: string; + organizationId?: string; + updatedAt?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "organizationId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "teamMember"; + update: { + createdAt?: null | number; + teamId?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "teamId" | "userId" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "organization"; + update: { + createdAt?: number; + logo?: null | string; + metadata?: null | string; + name?: string; + slug?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "slug" + | "logo" + | "createdAt" + | "metadata" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "member"; + update: { + createdAt?: number; + organizationId?: string; + role?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "userId" + | "role" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "invitation"; + update: { + email?: string; + expiresAt?: number; + inviterId?: string; + organizationId?: string; + role?: null | string; + status?: string; + teamId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "email" + | "role" + | "teamId" + | "status" + | "expiresAt" + | "inviterId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ssoProvider"; + update: { + domain?: string; + issuer?: string; + oidcConfig?: null | string; + organizationId?: null | string; + providerId?: string; + samlConfig?: null | string; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "issuer" + | "oidcConfig" + | "samlConfig" + | "userId" + | "providerId" + | "organizationId" + | "domain" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "jwks"; + update: { + createdAt?: number; + privateKey?: string; + publicKey?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "publicKey" | "privateKey" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "subscription"; + update: { + cancelAtPeriodEnd?: null | boolean; + periodEnd?: null | number; + periodStart?: null | number; + plan?: string; + referenceId?: string; + seats?: null | number; + status?: null | string; + stripeCustomerId?: null | string; + stripeSubscriptionId?: null | string; + trialEnd?: null | number; + trialStart?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "plan" + | "referenceId" + | "stripeCustomerId" + | "stripeSubscriptionId" + | "status" + | "periodStart" + | "periodEnd" + | "trialStart" + | "trialEnd" + | "cancelAtPeriodEnd" + | "seats" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "walletAddress"; + update: { + address?: string; + chainId?: number; + createdAt?: number; + isPrimary?: null | boolean; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "userId" + | "address" + | "chainId" + | "isPrimary" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "rateLimit"; + update: { + count?: null | number; + key?: null | string; + lastRequest?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ratelimit"; + update: { count?: number; key?: string; lastRequest?: number }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }; + onUpdateHandle?: string; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + }, + any + >; + updateOne: FunctionReference< + "mutation", + "internal", + { + input: + | { + model: "user"; + update: { + createdAt?: number; + displayUsername?: null | string; + email?: string; + emailVerified?: boolean; + image?: null | string; + isAnonymous?: null | boolean; + name?: string; + phoneNumber?: null | string; + phoneNumberVerified?: null | boolean; + twoFactorEnabled?: null | boolean; + updatedAt?: number; + userId?: null | string; + username?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "email" + | "emailVerified" + | "image" + | "createdAt" + | "updatedAt" + | "twoFactorEnabled" + | "isAnonymous" + | "username" + | "displayUsername" + | "phoneNumber" + | "phoneNumberVerified" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "session"; + update: { + createdAt?: number; + expiresAt?: number; + ipAddress?: null | string; + token?: string; + updatedAt?: number; + userAgent?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "expiresAt" + | "token" + | "createdAt" + | "updatedAt" + | "ipAddress" + | "userAgent" + | "userId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "account"; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + accountId?: string; + createdAt?: number; + idToken?: null | string; + password?: null | string; + providerId?: string; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scope?: null | string; + updatedAt?: number; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accountId" + | "providerId" + | "userId" + | "accessToken" + | "refreshToken" + | "idToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "scope" + | "password" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "verification"; + update: { + createdAt?: number; + expiresAt?: number; + identifier?: string; + updatedAt?: number; + value?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "identifier" + | "value" + | "expiresAt" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "twoFactor"; + update: { + backupCodes?: string; + secret?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "secret" | "backupCodes" | "userId" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "passkey"; + update: { + aaguid?: null | string; + backedUp?: boolean; + counter?: number; + createdAt?: null | number; + credentialID?: string; + deviceType?: string; + name?: null | string; + publicKey?: string; + transports?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "publicKey" + | "userId" + | "credentialID" + | "counter" + | "deviceType" + | "backedUp" + | "transports" + | "createdAt" + | "aaguid" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthApplication"; + update: { + clientId?: null | string; + clientSecret?: null | string; + createdAt?: null | number; + disabled?: null | boolean; + icon?: null | string; + metadata?: null | string; + name?: null | string; + redirectURLs?: null | string; + type?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "icon" + | "metadata" + | "clientId" + | "clientSecret" + | "redirectURLs" + | "type" + | "disabled" + | "userId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthAccessToken"; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + clientId?: null | string; + createdAt?: null | number; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "accessToken" + | "refreshToken" + | "accessTokenExpiresAt" + | "refreshTokenExpiresAt" + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthConsent"; + update: { + clientId?: null | string; + consentGiven?: null | boolean; + createdAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "clientId" + | "userId" + | "scopes" + | "createdAt" + | "updatedAt" + | "consentGiven" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "team"; + update: { + createdAt?: number; + name?: string; + organizationId?: string; + updatedAt?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "organizationId" + | "createdAt" + | "updatedAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "teamMember"; + update: { + createdAt?: null | number; + teamId?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "teamId" | "userId" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "organization"; + update: { + createdAt?: number; + logo?: null | string; + metadata?: null | string; + name?: string; + slug?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "name" + | "slug" + | "logo" + | "createdAt" + | "metadata" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "member"; + update: { + createdAt?: number; + organizationId?: string; + role?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "userId" + | "role" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "invitation"; + update: { + email?: string; + expiresAt?: number; + inviterId?: string; + organizationId?: string; + role?: null | string; + status?: string; + teamId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "organizationId" + | "email" + | "role" + | "teamId" + | "status" + | "expiresAt" + | "inviterId" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ssoProvider"; + update: { + domain?: string; + issuer?: string; + oidcConfig?: null | string; + organizationId?: null | string; + providerId?: string; + samlConfig?: null | string; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "issuer" + | "oidcConfig" + | "samlConfig" + | "userId" + | "providerId" + | "organizationId" + | "domain" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "jwks"; + update: { + createdAt?: number; + privateKey?: string; + publicKey?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "publicKey" | "privateKey" | "createdAt" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "subscription"; + update: { + cancelAtPeriodEnd?: null | boolean; + periodEnd?: null | number; + periodStart?: null | number; + plan?: string; + referenceId?: string; + seats?: null | number; + status?: null | string; + stripeCustomerId?: null | string; + stripeSubscriptionId?: null | string; + trialEnd?: null | number; + trialStart?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "plan" + | "referenceId" + | "stripeCustomerId" + | "stripeSubscriptionId" + | "status" + | "periodStart" + | "periodEnd" + | "trialStart" + | "trialEnd" + | "cancelAtPeriodEnd" + | "seats" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "walletAddress"; + update: { + address?: string; + chainId?: number; + createdAt?: number; + isPrimary?: null | boolean; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: + | "userId" + | "address" + | "chainId" + | "isPrimary" + | "createdAt" + | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "rateLimit"; + update: { + count?: null | number; + key?: null | string; + lastRequest?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ratelimit"; + update: { count?: number; key?: string; lastRequest?: number }; + where?: Array<{ + connector?: "AND" | "OR"; + field: "key" | "count" | "lastRequest" | "id"; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }; + onUpdateHandle?: string; + }, + any + >; + }; + adapterTest: { + count: FunctionReference<"query", "internal", any, any>; + create: FunctionReference<"mutation", "internal", any, any>; + delete: FunctionReference<"mutation", "internal", any, any>; + deleteMany: FunctionReference<"mutation", "internal", any, any>; + findMany: FunctionReference<"query", "internal", any, any>; + findOne: FunctionReference<"query", "internal", any, any>; + update: FunctionReference<"mutation", "internal", any, any>; + updateMany: FunctionReference<"mutation", "internal", any, any>; + }; + lib: { + create: FunctionReference< + "mutation", + "internal", + { + input: + | { + data: { + createdAt: number; + displayUsername?: null | string; + email: string; + emailVerified: boolean; + image?: null | string; + isAnonymous?: null | boolean; + name: string; + phoneNumber?: null | string; + phoneNumberVerified?: null | boolean; + twoFactorEnabled?: null | boolean; + updatedAt: number; + userId?: null | string; + username?: null | string; + }; + model: "user"; + } + | { + data: { + createdAt: number; + expiresAt: number; + ipAddress?: null | string; + token: string; + updatedAt: number; + userAgent?: null | string; + userId: string; + }; + model: "session"; + } + | { + data: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + accountId: string; + createdAt: number; + idToken?: null | string; + password?: null | string; + providerId: string; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scope?: null | string; + updatedAt: number; + userId: string; + }; + model: "account"; + } + | { + data: { + createdAt: number; + expiresAt: number; + identifier: string; + updatedAt: number; + value: string; + }; + model: "verification"; + } + | { + data: { backupCodes: string; secret: string; userId: string }; + model: "twoFactor"; + } + | { + data: { + aaguid?: null | string; + backedUp: boolean; + counter: number; + createdAt?: null | number; + credentialID: string; + deviceType: string; + name?: null | string; + publicKey: string; + transports?: null | string; + userId: string; + }; + model: "passkey"; + } + | { + data: { + clientId?: null | string; + clientSecret?: null | string; + createdAt?: null | number; + disabled?: null | boolean; + icon?: null | string; + metadata?: null | string; + name?: null | string; + redirectURLs?: null | string; + type?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + model: "oauthApplication"; + } + | { + data: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + clientId?: null | string; + createdAt?: null | number; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + model: "oauthAccessToken"; + } + | { + data: { + clientId?: null | string; + consentGiven?: null | boolean; + createdAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + model: "oauthConsent"; + } + | { + data: { + createdAt: number; + name: string; + organizationId: string; + updatedAt?: null | number; + }; + model: "team"; + } + | { + data: { + createdAt?: null | number; + teamId: string; + userId: string; + }; + model: "teamMember"; + } + | { + data: { + createdAt: number; + logo?: null | string; + metadata?: null | string; + name: string; + slug?: null | string; + }; + model: "organization"; + } + | { + data: { + createdAt: number; + organizationId: string; + role: string; + userId: string; + }; + model: "member"; + } + | { + data: { + email: string; + expiresAt: number; + inviterId: string; + organizationId: string; + role?: null | string; + status: string; + teamId?: null | string; + }; + model: "invitation"; + } + | { + data: { + domain: string; + issuer: string; + oidcConfig?: null | string; + organizationId?: null | string; + providerId: string; + samlConfig?: null | string; + userId?: null | string; + }; + model: "ssoProvider"; + } + | { + data: { + createdAt: number; + privateKey: string; + publicKey: string; + }; + model: "jwks"; + } + | { + data: { + cancelAtPeriodEnd?: null | boolean; + periodEnd?: null | number; + periodStart?: null | number; + plan: string; + referenceId: string; + seats?: null | number; + status?: null | string; + stripeCustomerId?: null | string; + stripeSubscriptionId?: null | string; + trialEnd?: null | number; + trialStart?: null | number; + }; + model: "subscription"; + } + | { + data: { + address: string; + chainId: number; + createdAt: number; + isPrimary?: null | boolean; + userId: string; + }; + model: "walletAddress"; + } + | { + data: { + count?: null | number; + key?: null | string; + lastRequest?: null | number; + }; + model: "rateLimit"; + } + | { + data: { count: number; key: string; lastRequest: number }; + model: "ratelimit"; + }; + }, + any + >; + deleteMany: FunctionReference< + "mutation", + "internal", + { + limit?: number; + model: string; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }, + any + >; + deleteOne: FunctionReference< + "mutation", + "internal", + { + limit?: number; + model: string; + offset?: number; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }, + any + >; + findMany: FunctionReference< + "query", + "internal", + { + limit?: number; + model: string; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }, + any + >; + findOne: FunctionReference< + "query", + "internal", + { + limit?: number; + model: string; + offset?: number; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }, + any + >; + getCurrentSession: FunctionReference<"query", "internal", {}, any>; + updateMany: FunctionReference< + "mutation", + "internal", + { + input: + | { + limit?: number; + model: "user"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: number; + displayUsername?: null | string; + email?: string; + emailVerified?: boolean; + image?: null | string; + isAnonymous?: null | boolean; + name?: string; + phoneNumber?: null | string; + phoneNumberVerified?: null | boolean; + twoFactorEnabled?: null | boolean; + updatedAt?: number; + userId?: null | string; + username?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "session"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: number; + expiresAt?: number; + ipAddress?: null | string; + token?: string; + updatedAt?: number; + userAgent?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "account"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + accountId?: string; + createdAt?: number; + idToken?: null | string; + password?: null | string; + providerId?: string; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scope?: null | string; + updatedAt?: number; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "verification"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: number; + expiresAt?: number; + identifier?: string; + updatedAt?: number; + value?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "twoFactor"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + backupCodes?: string; + secret?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "passkey"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + aaguid?: null | string; + backedUp?: boolean; + counter?: number; + createdAt?: null | number; + credentialID?: string; + deviceType?: string; + name?: null | string; + publicKey?: string; + transports?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "oauthApplication"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + clientId?: null | string; + clientSecret?: null | string; + createdAt?: null | number; + disabled?: null | boolean; + icon?: null | string; + metadata?: null | string; + name?: null | string; + redirectURLs?: null | string; + type?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "oauthAccessToken"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + clientId?: null | string; + createdAt?: null | number; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "oauthConsent"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + clientId?: null | string; + consentGiven?: null | boolean; + createdAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "team"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: number; + name?: string; + organizationId?: string; + updatedAt?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "teamMember"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: null | number; + teamId?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "organization"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: number; + logo?: null | string; + metadata?: null | string; + name?: string; + slug?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "member"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: number; + organizationId?: string; + role?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "invitation"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + email?: string; + expiresAt?: number; + inviterId?: string; + organizationId?: string; + role?: null | string; + status?: string; + teamId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "ssoProvider"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + domain?: string; + issuer?: string; + oidcConfig?: null | string; + organizationId?: null | string; + providerId?: string; + samlConfig?: null | string; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "jwks"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + createdAt?: number; + privateKey?: string; + publicKey?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "subscription"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + cancelAtPeriodEnd?: null | boolean; + periodEnd?: null | number; + periodStart?: null | number; + plan?: string; + referenceId?: string; + seats?: null | number; + status?: null | string; + stripeCustomerId?: null | string; + stripeSubscriptionId?: null | string; + trialEnd?: null | number; + trialStart?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "walletAddress"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + address?: string; + chainId?: number; + createdAt?: number; + isPrimary?: null | boolean; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "rateLimit"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { + count?: null | number; + key?: null | string; + lastRequest?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + limit?: number; + model: "ratelimit"; + offset?: number; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + select?: Array; + sortBy?: { direction: "asc" | "desc"; field: string }; + unique?: boolean; + update: { count?: number; key?: string; lastRequest?: number }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }; + }, + any + >; + updateOne: FunctionReference< + "mutation", + "internal", + { + input: + | { + model: "user"; + update: { + createdAt?: number; + displayUsername?: null | string; + email?: string; + emailVerified?: boolean; + image?: null | string; + isAnonymous?: null | boolean; + name?: string; + phoneNumber?: null | string; + phoneNumberVerified?: null | boolean; + twoFactorEnabled?: null | boolean; + updatedAt?: number; + userId?: null | string; + username?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "session"; + update: { + createdAt?: number; + expiresAt?: number; + ipAddress?: null | string; + token?: string; + updatedAt?: number; + userAgent?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "account"; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + accountId?: string; + createdAt?: number; + idToken?: null | string; + password?: null | string; + providerId?: string; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scope?: null | string; + updatedAt?: number; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "verification"; + update: { + createdAt?: number; + expiresAt?: number; + identifier?: string; + updatedAt?: number; + value?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "twoFactor"; + update: { + backupCodes?: string; + secret?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "passkey"; + update: { + aaguid?: null | string; + backedUp?: boolean; + counter?: number; + createdAt?: null | number; + credentialID?: string; + deviceType?: string; + name?: null | string; + publicKey?: string; + transports?: null | string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthApplication"; + update: { + clientId?: null | string; + clientSecret?: null | string; + createdAt?: null | number; + disabled?: null | boolean; + icon?: null | string; + metadata?: null | string; + name?: null | string; + redirectURLs?: null | string; + type?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthAccessToken"; + update: { + accessToken?: null | string; + accessTokenExpiresAt?: null | number; + clientId?: null | string; + createdAt?: null | number; + refreshToken?: null | string; + refreshTokenExpiresAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "oauthConsent"; + update: { + clientId?: null | string; + consentGiven?: null | boolean; + createdAt?: null | number; + scopes?: null | string; + updatedAt?: null | number; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "team"; + update: { + createdAt?: number; + name?: string; + organizationId?: string; + updatedAt?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "teamMember"; + update: { + createdAt?: null | number; + teamId?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "organization"; + update: { + createdAt?: number; + logo?: null | string; + metadata?: null | string; + name?: string; + slug?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "member"; + update: { + createdAt?: number; + organizationId?: string; + role?: string; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "invitation"; + update: { + email?: string; + expiresAt?: number; + inviterId?: string; + organizationId?: string; + role?: null | string; + status?: string; + teamId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ssoProvider"; + update: { + domain?: string; + issuer?: string; + oidcConfig?: null | string; + organizationId?: null | string; + providerId?: string; + samlConfig?: null | string; + userId?: null | string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "jwks"; + update: { + createdAt?: number; + privateKey?: string; + publicKey?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "subscription"; + update: { + cancelAtPeriodEnd?: null | boolean; + periodEnd?: null | number; + periodStart?: null | number; + plan?: string; + referenceId?: string; + seats?: null | number; + status?: null | string; + stripeCustomerId?: null | string; + stripeSubscriptionId?: null | string; + trialEnd?: null | number; + trialStart?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "walletAddress"; + update: { + address?: string; + chainId?: number; + createdAt?: number; + isPrimary?: null | boolean; + userId?: string; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "rateLimit"; + update: { + count?: null | number; + key?: null | string; + lastRequest?: null | number; + }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + } + | { + model: "ratelimit"; + update: { count?: number; key?: string; lastRequest?: number }; + where?: Array<{ + connector?: "AND" | "OR"; + field: string; + operator?: + | "lt" + | "lte" + | "gt" + | "gte" + | "eq" + | "in" + | "ne" + | "contains" + | "starts_with" + | "ends_with"; + value: + | string + | number + | boolean + | Array + | Array + | null; + }>; + }; + }, + any + >; + }; + }; }; diff --git a/convex/feedback.ts b/convex/feedback.ts deleted file mode 100644 index 38bcf74..0000000 --- a/convex/feedback.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getAuthUserId } from "@convex-dev/auth/server"; -import { v } from "convex/values"; -import { mutation } from "./_generated/server"; -// import { Feedback } from './schema/feedback'; - -export const createFeedback = mutation({ - args: { message: v.string() }, - returns: v.null(), - handler: async (ctx, { message }) => { - const userId = await getAuthUserId(ctx); - if (!userId) { - return null; - } - await ctx.db.insert("feedback", { - userId, - message, - createdAt: Date.now(), - }); - return null; - }, -}); diff --git a/convex/http.ts b/convex/http.ts index ba37638..78fd055 100644 --- a/convex/http.ts +++ b/convex/http.ts @@ -1,11 +1,12 @@ import { httpRouter } from "convex/server"; import { internal } from "./_generated/api"; -import { auth } from "./auth"; +import { authComponent, createAuth } from "./auth"; import { polar } from "./polar"; const http = httpRouter(); -auth.addHttpRoutes(http); +// Register Better Auth routes +authComponent.registerRoutes(http, createAuth); // Register Polar webhook with subscription event callbacks polar.registerRoutes(http, { diff --git a/convex/schema.ts b/convex/schema.ts index 5589223..232fc28 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -1,9 +1,7 @@ -import { authTables } from "@convex-dev/auth/server"; import { defineSchema, defineTable } from "convex/server"; import { Chat } from "./schema/chat"; import { ChatAttachment } from "./schema/chat_attachment"; import { Connector } from "./schema/connectors"; -import { Feedback } from "./schema/feedback"; import { Message } from "./schema/message"; import { ScheduledTask } from "./schema/scheduled_task"; import { TaskHistory } from "./schema/task_history"; @@ -13,7 +11,6 @@ import { User } from "./schema/user"; import { UserApiKey } from "./schema/user_api_key"; export default defineSchema({ - ...authTables, users: defineTable(User).index("email", ["email"]), chats: defineTable(Chat).index("by_user", ["userId"]), messages: defineTable(Message) @@ -23,7 +20,6 @@ export default defineSchema({ searchField: "content", filterFields: ["userId", "chatId"], }), - feedback: defineTable(Feedback).index("by_user", ["userId"]), chat_attachments: defineTable(ChatAttachment) .index("by_chatId", ["chatId"]) .index("by_userId", ["userId"]) diff --git a/convex/schema/feedback.ts b/convex/schema/feedback.ts deleted file mode 100644 index 4aff8d1..0000000 --- a/convex/schema/feedback.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { v } from "convex/values"; - -export const Feedback = v.object({ - userId: v.id("users"), - message: v.string(), - createdAt: v.optional(v.number()), -}); diff --git a/convex/schema/user.ts b/convex/schema/user.ts index 54fc536..655ff75 100644 --- a/convex/schema/user.ts +++ b/convex/schema/user.ts @@ -1,11 +1,15 @@ import { v } from "convex/values"; export const User = v.object({ + // Temporarily keep email for migration - allows onCreate trigger to find existing users + email: v.optional(v.string()), + // TEMPORARY: Keep these fields as optional for migration - will be removed after cleanup name: v.optional(v.string()), image: v.optional(v.string()), - email: v.optional(v.string()), - emailVerificationTime: v.optional(v.number()), isAnonymous: v.optional(v.boolean()), + emailVerificationTime: v.optional(v.number()), + // App-specific user preferences and profile data + // Other identity fields are managed by Better Auth preferredModel: v.optional(v.string()), preferredName: v.optional(v.string()), occupation: v.optional(v.string()), diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 0d9c9cf..d062652 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -7,6 +7,6 @@ if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_KEY) { capture_pageview: "history_change", capture_pageleave: true, // Enable pageleave capture capture_exceptions: true, // This enables capturing exceptions using Error Tracking - debug: process.env.NODE_ENV === "development", + debug: false, }); } diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 9e498a5..0000000 --- a/middleware.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { convexAuthNextjsMiddleware } from "@convex-dev/auth/nextjs/server"; - -export default convexAuthNextjsMiddleware(); - -export const config = { - matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], -}; From a393324bff34135f352dca0c5a12bcec727af4d3 Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Mon, 22 Sep 2025 02:18:09 +0100 Subject: [PATCH 07/10] feat(auth): implement Better Auth user deletion with comprehensive cleanup Migrated from manual deletion mutation to Better Auth's built-in user deletion with enhanced onDelete trigger for comprehensive data cleanup. Changes: - Enable Better Auth deleteUser feature in auth configuration - Move all cleanup logic to onDelete trigger in convex/auth.ts - Add cleanup for previously missed data: API keys, connectors, scheduled tasks - Update settings page to use authClient.deleteUser() instead of mutation - Remove manual deleteAccount mutation from convex/users.ts - Maintain concurrent deletion patterns and R2 file cleanup Benefits: - Built-in password verification and session management - Automatic session invalidation on deletion - Single source of truth for deletion logic in onDelete trigger - Comprehensive cleanup of ALL user data including previously missed tables - Framework-compliant user deletion flow Fixes comment about Better Auth user deletion not triggering onDelete. --- app/settings/page.tsx | 19 +++++--- convex/auth.ts | 100 ++++++++++++++++++++++++++++++++++++++++-- convex/users.ts | 74 ++----------------------------- 3 files changed, 112 insertions(+), 81 deletions(-) diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 2eb27b9..4a2a970 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,7 +1,7 @@ "use client"; import { CircleNotch, Headset, Rocket, Sparkle } from "@phosphor-icons/react"; -import { useAction, useMutation } from "convex/react"; +import { useAction } from "convex/react"; import { useRouter } from "next/navigation"; import { useCallback, useState } from "react"; import { MessageUsageCard } from "@/app/components/layout/settings/message-usage-card"; @@ -17,16 +17,16 @@ import { } from "@/components/ui/dialog"; import { toast } from "@/components/ui/toast"; import { api } from "@/convex/_generated/api"; +import { authClient } from "@/lib/auth-client"; export default function AccountSettingsPage() { const [isDeleting, setIsDeleting] = useState(false); const [showDeleteAccountDialog, setShowDeleteAccountDialog] = useState(false); - const deleteAccount = useMutation(api.users.deleteAccount); const generateCheckoutLink = useAction(api.polar.generateCheckoutLink); const generateCustomerPortalUrl = useAction( api.polar.generateCustomerPortalUrl ); - const { signOut, hasPremium, products } = useUser(); + const { hasPremium, products } = useUser(); const router = useRouter(); // Handle upgrade button click @@ -76,20 +76,25 @@ export default function AccountSettingsPage() { const confirmDeleteAccount = useCallback(async () => { setIsDeleting(true); try { - await deleteAccount({}); - await signOut(); + // Use Better Auth's user deletion which will trigger the onDelete trigger + // This handles comprehensive cleanup automatically + await authClient.deleteUser(); + + // No need to manually sign out as Better Auth handles session cleanup toast({ title: "Account deleted", status: "success" }); router.push("/"); - } catch { + } catch (_error) { toast({ title: "Failed to delete account", + description: + "Please try again or contact support if the problem persists.", status: "error", }); } finally { setIsDeleting(false); setShowDeleteAccountDialog(false); } - }, [deleteAccount, signOut, router]); + }, [router]); // Render the subscription button - memoize to prevent unnecessary re-renders const renderSubscriptionButton = useCallback(() => { diff --git a/convex/auth.ts b/convex/auth.ts index 4cb8d8a..ddd72f7 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -4,6 +4,7 @@ import { type GenericCtx, } from "@convex-dev/better-auth"; import { convex } from "@convex-dev/better-auth/plugins"; +import { R2 } from "@convex-dev/r2"; import { betterAuth } from "better-auth"; import { anonymous } from "better-auth/plugins"; import { MODEL_DEFAULT, RECOMMENDED_MODELS } from "../lib/config"; @@ -93,10 +94,98 @@ export const authComponent = createClient(components.betterAuth, { // Handle user updates if needed }, onDelete: async (ctx, authUser) => { - // Clean up when user is deleted - if (authUser.userId) { - await ctx.db.delete(authUser.userId as Id<"users">); + // Comprehensive cleanup when user is deleted via Better Auth + if (!authUser.userId) { + return; } + + const userId = authUser.userId as Id<"users">; + + // --- Step 1: Fetch all documents that need to be deleted in parallel --- + const [ + attachments, + messages, + chats, + usage, + apiKeys, + connectors, + scheduledTasks, + ] = await Promise.all([ + ctx.db + .query("chat_attachments") + .withIndex("by_userId", (q) => q.eq("userId", userId)) + .collect(), + ctx.db + .query("messages") + .withIndex("by_user", (q) => q.eq("userId", userId)) + .collect(), + ctx.db + .query("chats") + .withIndex("by_user", (q) => q.eq("userId", userId)) + .collect(), + ctx.db + .query("usage_history") + .withIndex("by_user", (q) => q.eq("userId", userId)) + .collect(), + ctx.db + .query("user_api_keys") + .withIndex("by_user_provider", (q) => q.eq("userId", userId)) + .collect(), + ctx.db + .query("connectors") + .withIndex("by_user", (q) => q.eq("userId", userId)) + .collect(), + ctx.db + .query("scheduled_tasks") + .withIndex("by_user", (q) => q.eq("userId", userId)) + .collect(), + ]); + + // --- Step 2: Collect all deletion promises and execute them concurrently --- + const deletionPromises: Promise[] = []; + + // Delete attachments and their files + const r2 = new R2(components.r2); + for (const att of attachments) { + deletionPromises.push( + r2.deleteObject(ctx, att.key).catch(() => { + // Silently handle storage deletion errors + }) + ); + deletionPromises.push( + ctx.db.delete(att._id).catch(() => { + // Silently handle database deletion errors + }) + ); + } + + // Delete messages + deletionPromises.push(...messages.map((msg) => ctx.db.delete(msg._id))); + + // Delete chats + deletionPromises.push(...chats.map((chat) => ctx.db.delete(chat._id))); + + // Delete usage history + deletionPromises.push(...usage.map((u) => ctx.db.delete(u._id))); + + // Delete API keys + deletionPromises.push(...apiKeys.map((key) => ctx.db.delete(key._id))); + + // Delete connectors + deletionPromises.push( + ...connectors.map((conn) => ctx.db.delete(conn._id)) + ); + + // Delete scheduled tasks (task history will be cleaned up via cascade) + deletionPromises.push( + ...scheduledTasks.map((task) => ctx.db.delete(task._id)) + ); + + // Execute all deletions concurrently + await Promise.allSettled(deletionPromises); + + // Finally delete user record + await ctx.db.delete(userId); }, }, }, @@ -126,6 +215,11 @@ export const createAuth = ( maxAge: 60 * 60, // Cache duration in seconds (1 hour) }, }, + user: { + deleteUser: { + enabled: true, + }, + }, plugins: [ // Anonymous authentication anonymous({ diff --git a/convex/users.ts b/convex/users.ts index 2fc0b85..ca6e54a 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -1,4 +1,3 @@ -import { R2 } from "@convex-dev/r2"; import { calculateRateLimit, type RateLimitConfig, @@ -6,7 +5,6 @@ import { import { ConvexError, v } from "convex/values"; import { MODEL_DEFAULT } from "../lib/config"; import { ERROR_CODES } from "../lib/error-codes"; -import { components } from "./_generated/api"; import type { Id } from "./_generated/dataModel"; import { internalMutation, @@ -619,75 +617,9 @@ export const getRateLimitStatus = query({ // The application no longer automatically merges anonymous accounts with Google accounts // for security and simplicity. Users who sign in with Google will start with fresh accounts. -// Delete account and all associated data -export const deleteAccount = mutation({ - args: {}, - returns: v.null(), - handler: async (ctx) => { - const authUser = await authComponent.safeGetAuthUser(ctx); - if (!authUser?.userId) { - throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); - } - const userId = authUser.userId as Id<"users">; - - // --- Step 1: Fetch all documents that need to be deleted in parallel --- - const [attachments, messages, chats, usage] = await Promise.all([ - ctx.db - .query("chat_attachments") - .withIndex("by_userId", (q) => q.eq("userId", userId)) - .collect(), - ctx.db - .query("messages") - .withIndex("by_user", (q) => q.eq("userId", userId)) - .collect(), - ctx.db - .query("chats") - .withIndex("by_user", (q) => q.eq("userId", userId)) - .collect(), - ctx.db - .query("usage_history") - .withIndex("by_user", (q) => q.eq("userId", userId)) - .collect(), - ]); - - // --- Step 2: Collect all deletion promises and execute them concurrently --- - const deletionPromises: Promise[] = []; - - // Delete attachments and their files - const r2 = new R2(components.r2); - for (const att of attachments) { - deletionPromises.push( - r2.deleteObject(ctx, att.key).catch(() => { - // Silently handle storage deletion errors - }) - ); - deletionPromises.push( - ctx.db.delete(att._id).catch(() => { - // Silently handle database deletion errors - }) - ); - } - - // Delete messages - deletionPromises.push(...messages.map((msg) => ctx.db.delete(msg._id))); - - // Delete chats - deletionPromises.push(...chats.map((chat) => ctx.db.delete(chat._id))); - - // Delete usage history - deletionPromises.push(...usage.map((u) => ctx.db.delete(u._id))); - - // Note: Better Auth manages its own user deletion through the onDelete trigger - // in convex/auth.ts, so we don't need to manually delete auth tables - - // Execute all deletions concurrently - await Promise.allSettled(deletionPromises); - - // Finally delete user record - await ctx.db.delete(userId); - return null; - }, -}); +// Note: User account deletion is now handled by Better Auth's deleteUser() method. +// This triggers the onDelete trigger in convex/auth.ts which performs comprehensive +// cleanup of all user data including chats, messages, attachments, API keys, etc. // Internal query to get user by ID export const getUser = internalQuery({ From 147d0df6a810752d2bbfcc85ba8d24ea709e3253 Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Mon, 22 Sep 2025 02:18:53 +0100 Subject: [PATCH 08/10] fix(auth): check token authentication for chat and create-chat endpoints --- app/api/chat/route.ts | 5 +++++ app/api/create-chat/route.ts | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index ad2ff50..5f9a79c 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -509,6 +509,11 @@ export async function POST(req: Request) { const token = await getToken(); + // If no valid token, the user is not authenticated + if (!token) { + return createErrorResponse(new Error("Unauthorized")); + } + // Get current user first (needed for multiple operations below) const user = await fetchQuery(api.users.getCurrentUser, {}, { token }); diff --git a/app/api/create-chat/route.ts b/app/api/create-chat/route.ts index eb9aca4..fcbf93f 100644 --- a/app/api/create-chat/route.ts +++ b/app/api/create-chat/route.ts @@ -27,9 +27,14 @@ export async function POST(request: Request) { const token = await getToken(); + // If no valid token, the user is not authenticated + if (!token) { + return createErrorResponse(new Error("Unauthorized")); + } + const user = await fetchQuery(api.users.getCurrentUser, {}, { token }); - // If the user is not authenticated or the token is invalid, short-circuit early + // If the user is not found, return unauthorized if (!user) { return createErrorResponse(new Error("Unauthorized")); } From abe5c2b9ac008b7a8d1ba7b150aff5938593f3af Mon Sep 17 00:00:00 2001 From: AjanRaj <55903526+ajanraj@users.noreply.github.com> Date: Mon, 22 Sep 2025 02:21:32 +0100 Subject: [PATCH 09/10] fix(a11y): add role="alert" to error message for screen reader announcement Add ARIA role="alert" to the error message in anonymous sign-in component to ensure screen readers properly announce error states to users. - Meets WCAG 2.1 AA 4.1.3 Status Messages criterion - Creates live region for immediate announcement - Improves accessibility for assistive technology users --- app/components/auth/anonymous-sign-in.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/auth/anonymous-sign-in.tsx b/app/components/auth/anonymous-sign-in.tsx index 8b1a66c..13d80b5 100644 --- a/app/components/auth/anonymous-sign-in.tsx +++ b/app/components/auth/anonymous-sign-in.tsx @@ -22,7 +22,9 @@ export function AnonymousSignIn() { if (error) { return (
      -

      {error}

      +

      + {error} +