From 610bf29a633cb25564b6147444a8fc06c0f456bc Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:29:34 +0300 Subject: [PATCH 1/6] fix(auth): migrate Better Auth integration to 1.7.1 --- .../components/integrations-settings.tsx | 7 +- .../app/(main)/settings/account/page.tsx | 3 +- .../account/sections/two-factor-dialog.tsx | 9 ++- apps/dashboard/package.json | 2 +- bun.lock | 80 +++++++++++++------ packages/ai/src/ai/tools/utils/oauth-token.ts | 10 +-- packages/auth/package.json | 8 +- packages/auth/src/auth.ts | 12 +-- packages/auth/src/client/auth-client.ts | 2 - packages/auth/src/rate-limit-storage.test.ts | 58 ++++++++++++++ packages/auth/src/rate-limit-storage.ts | 20 +++++ packages/db/src/drizzle/schema/auth.test.ts | 26 ++++++ packages/db/src/drizzle/schema/auth.ts | 6 ++ 13 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 packages/auth/src/rate-limit-storage.test.ts create mode 100644 packages/auth/src/rate-limit-storage.ts create mode 100644 packages/db/src/drizzle/schema/auth.test.ts diff --git a/apps/dashboard/app/(main)/organizations/components/integrations-settings.tsx b/apps/dashboard/app/(main)/organizations/components/integrations-settings.tsx index 0fff4114c..2397c6bf1 100644 --- a/apps/dashboard/app/(main)/organizations/components/integrations-settings.tsx +++ b/apps/dashboard/app/(main)/organizations/components/integrations-settings.tsx @@ -508,7 +508,12 @@ function GitHubIntegrationRow({ organizationId }: { organizationId: string }) { const disconnect = useMutation({ mutationFn: async () => { - const result = await authClient.unlinkAccount({ providerId: "github" }); + if (!githubAccount) { + throw new Error("GitHub account is not linked"); + } + const result = await authClient.unlinkAccount({ + accountId: githubAccount.id, + }); if (result.error) { throw new Error(result.error.message); } diff --git a/apps/dashboard/app/(main)/settings/account/page.tsx b/apps/dashboard/app/(main)/settings/account/page.tsx index 28d904a09..c250b62b4 100644 --- a/apps/dashboard/app/(main)/settings/account/page.tsx +++ b/apps/dashboard/app/(main)/settings/account/page.tsx @@ -447,8 +447,7 @@ export default function AccountSettingsPage() { const unlinkAccount = useMutation({ mutationFn: async (accountToUnlink: Account) => { const result = await authClient.unlinkAccount({ - providerId: accountToUnlink.providerId, - accountId: accountToUnlink.accountId, + accountId: accountToUnlink.id, }); if (result.error) { throw new Error(result.error.message); diff --git a/apps/dashboard/app/(main)/settings/account/sections/two-factor-dialog.tsx b/apps/dashboard/app/(main)/settings/account/sections/two-factor-dialog.tsx index bffe47229..1bdc4d231 100644 --- a/apps/dashboard/app/(main)/settings/account/sections/two-factor-dialog.tsx +++ b/apps/dashboard/app/(main)/settings/account/sections/two-factor-dialog.tsx @@ -120,18 +120,19 @@ export function TwoFactorDialog({ const enableMutation = useMutation({ mutationFn: async () => { - const result = await authClient.twoFactor.enable({ password }); + const result = await authClient.twoFactor.enable({ + method: "totp", + password, + }); if (result.error) { throw new Error(result.error.message); } return result.data; }, onSuccess: (data) => { - if (data?.totpURI) { + if (data?.method === "totp") { setTotpUri(data.totpURI); setSecret(extractSecretFromTotpUri(data.totpURI)); - } - if (data?.backupCodes) { setBackupCodes(data.backupCodes); } setStep("setup"); diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 3e297c750..2d9c9f07c 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -65,7 +65,7 @@ "atmn": "^1.1.8", "autumn-js": "catalog:", "babel-plugin-react-compiler": "^19.1.0-rc.1-rc-af1b7da-20250421", - "better-auth": "^1.5.5", + "better-auth": "1.7.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/bun.lock b/bun.lock index efd4f777b..708a32baf 100644 --- a/bun.lock +++ b/bun.lock @@ -169,7 +169,7 @@ "atmn": "^1.1.8", "autumn-js": "catalog:", "babel-plugin-react-compiler": "^19.1.0-rc.1-rc-af1b7da-20250421", - "better-auth": "^1.5.5", + "better-auth": "1.7.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -505,9 +505,9 @@ "name": "@databuddy/auth", "version": "0.0.1", "dependencies": { - "@better-auth/core": "^1.6.11", - "@better-auth/redis-storage": "^1.6.5", - "@better-auth/sso": "^1.4.10", + "@better-auth/core": "1.7.1", + "@better-auth/redis-storage": "1.7.1", + "@better-auth/sso": "1.7.1", "@databuddy/db": "*", "@databuddy/email": "*", "@databuddy/env": "workspace:*", @@ -515,7 +515,7 @@ "@databuddy/redis": "*", "@databuddy/services": "workspace:*", "@databuddy/shared": "workspace:*", - "better-auth": "^1.4.10", + "better-auth": "1.7.1", "drizzle-kit": "^1.0.0-rc.1", "evlog": "catalog:", "resend": "^4.8.0", @@ -1026,27 +1026,27 @@ "@base-ui-components/utils": ["@base-ui-components/utils@0.2.2", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-rNJCD6TFy3OSRDKVHJDzLpxO3esTV1/drRtWNUpe7rCpPN9HZVHUCuP+6rdDYDGWfXnQHbqi05xOyRP2iZAlkw=="], - "@better-auth/core": ["@better-auth/core@1.6.11", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-LrwidLCV8azdMGjvtwp30nj9tIv1BwI3VhtC0UaGSjQkAVWw4bN42I8qwbxRziPeSQoj+zUVkOpxZzAWBDARtQ=="], + "@better-auth/core": ["@better-auth/core@1.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.41.1", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-eZ9lqcnVLMZ3QtUByRo4VZqkB1ESyRddd9NfWjBdDPgh+jcwLScoIUAqhtHLR8zaSUJZah8OLGlkzObyPdUH7A=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-4jpkETIGZOHCf7BK4jnu22fdN6jjomH0/HhEzkaWy3+Eppi5PYlHTF/460jrTmA3Xc+Vqwp9t282ymHiEPypGw=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-qlqNyg5V9bXHSP68/vtlsiZayhR4hgvEGiS/E3SIj8bCpWWFGmyQkxJbQCqpBmC7vT30wE/kNtJMHIgnV3rkiw=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "kysely": "^0.28.17" }, "optionalPeers": ["kysely"] }, "sha512-/g8M9RfIjdcZDnbstSUvQiINkvdNlCeZr248zwqx2/PVksQI1MhQofbzUn3RnQnbPKp0EPwpX/dR3oudRFenUg=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-yWCpE1cZpMUj37nD6JFDK+GDR8zS37L5WI73il3qbU9TXtWsxUQKc/5c3IHsHizWQsmcQI8uv2pAFKxsRDa+AQ=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0" } }, "sha512-hpdfw0BBf8MuzLkIdmbcUZICbY9r/bhLO2RxSnkzT5+/O+0I0u2I8+m0YUP7vNllP/ZCKASHOYgXPLO75Z0f9Q=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2" } }, "sha512-6NX1yv88DeqdoG7owYFqKwlrDGaIPhsC52JUGrUgeGVKyOq8a/6hHlHsqG1C2FwT23SHQiKVYCEAG9N6aH5OvQ=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-3Tor8rSv8vSEIMEaV2PFpPEuVhqc1gNoZ6eGvoh3LwExXXuj8madew6ob+H1pH7Aphn3Ar5PQ08AguT8TbwFAA=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-9ILTcNqhG37QK//qR4UhYLyKzNqq6w6zVTf5KX6xkiTjNcV7Oh1yS31lkIJEVTqRNcy9AoV6FZMW6Bbsm8IMDA=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-Pw+7q7zTp+VSci1V+CYMvuxIbAeVMZLe4lRo46LJoAKMHfjFl5T/ycsyFvWs/DkWC7n9gZZzRDEbHp0I5FiKKw=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-ZiUcafQ85InAofcUjyGgCPjKLfQjXr9SvDmMjuFUW8oEbreA6C6GaFAEA77VuV2doZUQlzvQQ4gCoTmS28W92A=="], - "@better-auth/redis-storage": ["@better-auth/redis-storage@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "ioredis": "^5.0.0" } }, "sha512-xNlkb/p9MHEp0Wbpje/BaWA/CNJYMtjBV78aBq6csOxTchg0Nlam1vWBA0YXRCHoEl2TVW7HpySTp+Sj3DJHmw=="], + "@better-auth/redis-storage": ["@better-auth/redis-storage@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "ioredis": "^5.0.0" } }, "sha512-yr6ZnqIvL+3Enp4n4GA8a3agFPVQC19hHUGisMedNb1HzSzFzFcKUrUL0g0KjSxSm2mrIW0lhXSpHiPGtrf0QA=="], - "@better-auth/sso": ["@better-auth/sso@1.6.11", "", { "dependencies": { "fast-xml-parser": "^5.5.7", "jose": "^6.1.3", "samlify": "~2.10.2", "tldts": "^6.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "better-auth": "^1.6.11", "better-call": "1.3.5" } }, "sha512-lJHmoCayp9Woh/MPKTHDfGq7k1oQbU2yz5tIOZXl/pzrgLxV7fMGo9aJCyabHkw3GHMjBes4byC6aakHYzpZIg=="], + "@better-auth/sso": ["@better-auth/sso@1.7.1", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10", "fast-xml-parser": "^5.8.0", "jose": "^6.2.3", "samlify": "^2.13.1", "tldts": "^7.4.3", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "better-auth": "^1.7.1", "better-call": "1.4.0" } }, "sha512-fkGNMO8W5uNJSHAlvSe4Gxm1NVTQT4JFS7JpCNIxUAl5UyskE1yFXc2NH0KjcvSbR1RfgrqUgUevOjOjcSUKcg=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-hsjDHc8MZbm6/AHeNdtywrWedXevnBjmdvnHTcZub+rTVjOv+Td0roI8USKuC6uUibmrl//2rJfVCsGbopihNA=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-kLKjMfFlTbyt49DGeI9okHAsn0MtBZcMoQYKaEdgR0H3BHzqqyzePcQz/hxAmRgjB4p/6inise3zJwhX0sgXrQ=="], - "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], - "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], "@biomejs/biome": ["@biomejs/biome@2.5.1", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.1", "@biomejs/cli-darwin-x64": "2.5.1", "@biomejs/cli-linux-arm64": "2.5.1", "@biomejs/cli-linux-arm64-musl": "2.5.1", "@biomejs/cli-linux-x64": "2.5.1", "@biomejs/cli-linux-x64-musl": "2.5.1", "@biomejs/cli-win32-arm64": "2.5.1", "@biomejs/cli-win32-x64": "2.5.1" }, "bin": { "biome": "bin/biome" } }, "sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q=="], @@ -2636,7 +2636,7 @@ "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.9.12", "", {}, "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A=="], "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], @@ -2756,7 +2756,7 @@ "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - "better-auth": ["better-auth@1.6.11", "", { "dependencies": { "@better-auth/core": "1.6.11", "@better-auth/drizzle-adapter": "1.6.11", "@better-auth/kysely-adapter": "1.6.11", "@better-auth/memory-adapter": "1.6.11", "@better-auth/mongo-adapter": "1.6.11", "@better-auth/prisma-adapter": "1.6.11", "@better-auth/telemetry": "1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-Wwt6+q07dwIhsp6XiM7L1qSXVUWBEtNl+eZvwM778CguFqDZFBN9Pt6LtFaHl55t8Z+Zc//5kxcbgDY8/79vFQ=="], + "better-auth": ["better-auth@1.7.1", "", { "dependencies": { "@better-auth/core": "1.7.1", "@better-auth/drizzle-adapter": "1.7.1", "@better-auth/kysely-adapter": "1.7.1", "@better-auth/memory-adapter": "1.7.1", "@better-auth/mongo-adapter": "1.7.1", "@better-auth/prisma-adapter": "1.7.1", "@better-auth/telemetry": "1.7.1", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.2.0", "@noble/hashes": "^2.2.0", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.2.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.3.0", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-g8WlTQijxXWJjPVZfFu1+EJg9cwwHrKDmIkcYMzx8CzYA+tDxl6NI7qQbKkbgw5UtHILsT5VH+RMzFzwnVJqAg=="], "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], @@ -4546,7 +4546,7 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "samlify": ["samlify@2.10.2", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.6", "camelcase": "^6.2.0", "node-forge": "^1.3.0", "node-rsa": "^1.1.1", "pako": "^1.0.10", "uuid": "^8.3.2", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.32" } }, "sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA=="], + "samlify": ["samlify@2.13.1", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.11", "node-rsa": "^1.1.1", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.34" } }, "sha512-vdYr/zohDGBbfWNU4miEzc1jmWOtkLySPViapC6nfGkv9KxzLq4UlGkKyryzwLw4jVlZk88Rw93HaCRVpe+t+g=="], "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], @@ -4786,9 +4786,9 @@ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + "tldts": ["tldts@7.4.10", "", { "dependencies": { "tldts-core": "^7.4.10" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog=="], - "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + "tldts-core": ["tldts-core@7.4.10", "", {}, "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw=="], "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], @@ -5042,7 +5042,7 @@ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - "xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], + "xpath": ["xpath@0.0.34", "", {}, "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], @@ -5084,6 +5084,10 @@ "@anthropic-ai/tokenizer/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@authenio/xml-encryption/@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + + "@authenio/xml-encryption/xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1052.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.13", "@aws-sdk/nested-clients": "^3.997.11", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-QqZNB3so7UIDxZtroc85TQaLVxdZRFm0eWM1CSR2N+b06as9TOrilvrlTZuj3guYlxMs6yLOgGxnklJ5qMYtTw=="], "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], @@ -5116,8 +5120,12 @@ "@babel/template/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@better-auth/core/better-call": ["better-call@1.4.0", "", { "dependencies": { "@better-auth/utils": "^0.5.0", "@better-fetch/fetch": "^1.3.1", "rou3": "^0.9.1", "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA=="], + "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@better-auth/sso/better-call": ["better-call@1.4.0", "", { "dependencies": { "@better-auth/utils": "^0.5.0", "@better-fetch/fetch": "^1.3.1", "rou3": "^0.9.1", "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA=="], + "@better-auth/sso/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], @@ -5612,8 +5620,14 @@ "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "better-auth/better-call": ["better-call@1.4.0", "", { "dependencies": { "@better-auth/utils": "^0.5.0", "@better-fetch/fetch": "^1.3.1", "rou3": "^0.9.1", "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA=="], + "better-auth/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + + "better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "bun-types/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], @@ -5858,7 +5872,7 @@ "run-jxa/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], - "samlify/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "samlify/@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], @@ -5940,6 +5954,8 @@ "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "xml-crypto/@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + "xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="], "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -5962,6 +5978,18 @@ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@better-auth/core/better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], + + "@better-auth/core/better-call/rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="], + + "@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], + + "@better-auth/sso/better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], + + "@better-auth/sso/better-call/rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="], + + "@better-auth/sso/better-call/set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], + "@databuddy/ai/tokenlens/@tokenlens/core": ["@tokenlens/core@2.0.0-alpha.3", "", {}, "sha512-oR+fO2vpTP0/w7xfc/JhI1QRnwlvP0/UVa6vAilp7o8/Gl7iCemPH0Ga6WYuiGI5TL6JA/M1T5losFsK0vKmpA=="], "@databuddy/ai/tokenlens/@tokenlens/fetch": ["@tokenlens/fetch@2.0.0-alpha.3", "", { "dependencies": { "@tokenlens/core": "2.0.0-alpha.3" } }, "sha512-/v5QDXjfUCiQz4iu+CjG4R9qAhNkExfIM5H0Yo1+/jPzxrvB7HCu/60i0lZJQltJS44wEqlgA55qrohpZTY3tw=="], @@ -6382,6 +6410,12 @@ "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "better-auth/better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], + + "better-auth/better-call/rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="], + + "better-auth/better-call/set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], + "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], diff --git a/packages/ai/src/ai/tools/utils/oauth-token.ts b/packages/ai/src/ai/tools/utils/oauth-token.ts index 296d8125e..514ec1c75 100644 --- a/packages/ai/src/ai/tools/utils/oauth-token.ts +++ b/packages/ai/src/ai/tools/utils/oauth-token.ts @@ -12,7 +12,7 @@ const SCOPE_SEPARATOR = /[\s,]+/; interface TokenCandidate { accessToken: string | null; accessTokenExpiresAt: Date | null; - providerAccountId: string; + accountId: string; refreshToken: string | null; scope: string | null; userId: string; @@ -35,7 +35,6 @@ function hasScope(scope: string | null, required: string): boolean { } async function resolveCandidateToken( - providerId: string, candidate: TokenCandidate ): Promise { if (candidate.accessToken && !isExpired(candidate)) { @@ -50,8 +49,7 @@ async function resolveCandidateToken( try { const refreshed = await auth.api.getAccessToken({ body: { - providerId, - accountId: candidate.providerAccountId, + accountId: candidate.accountId, userId: candidate.userId, }, }); @@ -77,9 +75,9 @@ async function resolveOAuthToken( const candidates: TokenCandidate[] = await db .select({ + accountId: account.id, accessToken: account.accessToken, accessTokenExpiresAt: account.accessTokenExpiresAt, - providerAccountId: account.accountId, refreshToken: account.refreshToken, scope: account.scope, userId: account.userId, @@ -99,7 +97,7 @@ async function resolveOAuthToken( if (requiredScope && !hasScope(candidate.scope, requiredScope)) { continue; } - const resolved = await resolveCandidateToken(providerId, candidate); + const resolved = await resolveCandidateToken(candidate); if (resolved) { return resolved; } diff --git a/packages/auth/package.json b/packages/auth/package.json index c6dca4997..e5fd91e3e 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -14,9 +14,9 @@ "check-types": "tsc --noEmit" }, "dependencies": { - "@better-auth/core": "^1.6.11", - "@better-auth/redis-storage": "^1.6.5", - "@better-auth/sso": "^1.4.10", + "@better-auth/core": "1.7.1", + "@better-auth/redis-storage": "1.7.1", + "@better-auth/sso": "1.7.1", "@databuddy/db": "*", "@databuddy/email": "*", "@databuddy/env": "workspace:*", @@ -24,7 +24,7 @@ "@databuddy/redis": "*", "@databuddy/services": "workspace:*", "@databuddy/shared": "workspace:*", - "better-auth": "^1.4.10", + "better-auth": "1.7.1", "drizzle-kit": "^1.0.0-rc.1", "evlog": "catalog:", "resend": "^4.8.0" diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 58887fd98..7b32b6621 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -56,6 +56,7 @@ import { log } from "evlog"; import { Resend } from "resend"; import { ac, admin, member, owner, viewer } from "./permissions"; import { getAuthAuditContext } from "./audit-context"; +import { createAuthRateLimitStorage } from "./rate-limit-storage"; function generateOrgSlug(name: string): string { const base = name @@ -381,15 +382,7 @@ export const auth = betterAuth({ rateLimit: { window: 60, max: 100, - customStorage: { - get: async (key) => { - const value = await getRedisCache().get(key); - return value ? JSON.parse(value) : null; - }, - set: async (key, value) => { - await getRedisCache().set(key, JSON.stringify(value), "EX", 120); - }, - }, + customStorage: createAuthRateLimitStorage(), customRules: { "/sign-up/email": { window: 60, max: 3 }, "/sign-in/email": { window: 10, max: 3 }, @@ -532,6 +525,7 @@ export const auth = betterAuth({ }, }, appName: "databuddy.cc", + baseURL: config.urls.dashboard, onAPIError: { throw: false, onError: (error) => { diff --git a/packages/auth/src/client/auth-client.ts b/packages/auth/src/client/auth-client.ts index 969e822f8..d055fd7a8 100644 --- a/packages/auth/src/client/auth-client.ts +++ b/packages/auth/src/client/auth-client.ts @@ -2,7 +2,6 @@ import { ssoClient } from "@better-auth/sso/client"; import { customSessionClient, emailOTPClient, - genericOAuthClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, @@ -18,7 +17,6 @@ export const authClient = createAuthClient({ customSessionClient(), twoFactorClient(), multiSessionClient(), - genericOAuthClient(), emailOTPClient(), magicLinkClient(), lastLoginMethodClient(), diff --git a/packages/auth/src/rate-limit-storage.test.ts b/packages/auth/src/rate-limit-storage.test.ts new file mode 100644 index 000000000..9abd92bd4 --- /dev/null +++ b/packages/auth/src/rate-limit-storage.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, mock, test } from "bun:test"; + +const ratelimit = mock(async () => ({ + limit: 3, + remaining: 2, + reset: Date.now() + 60_000, + success: true, +})); + +mock.module("@databuddy/redis", () => ({ ratelimit })); + +const { createAuthRateLimitStorage } = await import("./rate-limit-storage"); + +describe("createAuthRateLimitStorage", () => { + test("atomically delegates the request rule and allows successful requests", async () => { + resetRateLimit(); + const storage = createAuthRateLimitStorage(); + + await expect( + storage.consume("auth:sign-in", { max: 3, window: 60 }) + ).resolves.toEqual({ allowed: true, retryAfter: null }); + expect(ratelimit).toHaveBeenCalledWith("auth:sign-in", 3, 60); + }); + + test("maps blocked requests to a positive retry delay", async () => { + resetRateLimit({ + success: false, + reset: Date.now() + 2_000, + }); + const storage = createAuthRateLimitStorage(); + + await expect( + storage.consume("auth:sign-in", { max: 3, window: 60 }) + ).resolves.toEqual({ allowed: false, retryAfter: 2 }); + }); + + test("never returns a zero-second retry delay", async () => { + resetRateLimit({ success: false, reset: Date.now() }); + const storage = createAuthRateLimitStorage(); + + await expect( + storage.consume("auth:sign-in", { max: 3, window: 60 }) + ).resolves.toEqual({ allowed: false, retryAfter: 1 }); + }); +}); + +function resetRateLimit( + override: Partial>> = {} +) { + ratelimit.mockReset(); + ratelimit.mockResolvedValue({ + limit: 3, + remaining: 2, + reset: Date.now() + 60_000, + success: true, + ...override, + }); +} diff --git a/packages/auth/src/rate-limit-storage.ts b/packages/auth/src/rate-limit-storage.ts new file mode 100644 index 000000000..429fff590 --- /dev/null +++ b/packages/auth/src/rate-limit-storage.ts @@ -0,0 +1,20 @@ +import { ratelimit } from "@databuddy/redis"; + +export interface AuthRateLimitRule { + max: number; + window: number; +} + +export function createAuthRateLimitStorage() { + return { + consume: async (key: string, rule: AuthRateLimitRule) => { + const result = await ratelimit(key, rule.max, rule.window); + return { + allowed: result.success, + retryAfter: result.success + ? null + : Math.max(1, Math.ceil((result.reset - Date.now()) / 1000)), + }; + }, + }; +} diff --git a/packages/db/src/drizzle/schema/auth.test.ts b/packages/db/src/drizzle/schema/auth.test.ts new file mode 100644 index 000000000..5e77371e9 --- /dev/null +++ b/packages/db/src/drizzle/schema/auth.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test"; +import { getTableConfig } from "drizzle-orm/pg-core"; +import { account } from "./auth"; + +describe("Better Auth 1.7 account identity schema", () => { + test("requires a trusted issuer on every account row", () => { + const issuer = getTableConfig(account).columns.find( + (column) => column.name === "issuer" + ); + + expect(issuer?.notNull).toBe(true); + expect(issuer?.dataType).toBe("string"); + }); + + test("uniquely scopes provider account IDs by issuer", () => { + const identityIndex = getTableConfig(account).indexes.find( + (index) => index.config.name === "accounts_issuer_account_unique" + ); + + expect(identityIndex?.config.unique).toBe(true); + expect(identityIndex?.config.columns.map((column) => column.name)).toEqual([ + "issuer", + "account_id", + ]); + }); +}); diff --git a/packages/db/src/drizzle/schema/auth.ts b/packages/db/src/drizzle/schema/auth.ts index e95bdad25..44cb8f1a7 100644 --- a/packages/db/src/drizzle/schema/auth.ts +++ b/packages/db/src/drizzle/schema/auth.ts @@ -128,6 +128,7 @@ export const account = pgTable( id: text().primaryKey().notNull(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), + issuer: text().notNull(), userId: text("user_id").notNull(), accessToken: text("access_token"), refreshToken: text("refresh_token"), @@ -165,6 +166,11 @@ export const account = pgTable( table.providerId.asc().nullsLast().op("text_ops"), table.accountId.asc().nullsLast().op("text_ops") ), + uniqueIndex("accounts_issuer_account_unique").using( + "btree", + table.issuer.asc().nullsLast().op("text_ops"), + table.accountId.asc().nullsLast().op("text_ops") + ), foreignKey({ columns: [table.userId], foreignColumns: [user.id], From f8437d79fc17d22516918be1dd69600425ecc154 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:29:41 +0300 Subject: [PATCH 2/6] docs(auth): add Better Auth 1.7 cutover runbook --- packages/auth/UPGRADE-1.7.md | 234 +++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 packages/auth/UPGRADE-1.7.md diff --git a/packages/auth/UPGRADE-1.7.md b/packages/auth/UPGRADE-1.7.md new file mode 100644 index 000000000..e655cd81d --- /dev/null +++ b/packages/auth/UPGRADE-1.7.md @@ -0,0 +1,234 @@ +# Better Auth 1.7 cutover runbook + +This repository is upgrading from the currently locked Better Auth `1.6.11` +family to `1.7.1`. The application changes in this branch must be deployed only +after the account identity cutover below has completed in the target database. + +This is intentionally an operator-run runbook. The repository currently uses +Drizzle schema push for PostgreSQL and does not have a checked-in PostgreSQL +migration runner. Do not use `bun run db:push` against staging or production for +this change: it cannot choose trusted issuers, migrate credential account IDs, +or stop on identity collisions. + +## What changed in the application + +- All directly used Better Auth packages are pinned to `1.7.1` together. +- `account.issuer` and the `(issuer, account_id)` unique index are represented + in the Drizzle schema. +- The Redis rate-limit adapter now uses Better Auth 1.7's atomic `consume` + contract. The existing Redis Lua sliding-window implementation remains the + counter and continues to fail open on Redis outages as before. +- Account-specific selectors now use the local Better Auth account row ID. + Provider account IDs remain display/token data, not selectors. +- The dashboard explicitly requests TOTP and narrows the new two-factor + response before reading TOTP-only fields. +- The dashboard auth instance has a canonical `baseURL` from the environment- + resolved dashboard URL, so callback and redirect URLs do not depend on a + proxy's forwarded host. +- Databuddy's custom API-key MCP server is not Better Auth MCP and does not + need the `@better-auth/mcp` migration. + +## Required order + +1. Prepare a staging database clone or snapshot. Never use customer data as a + test fixture or paste account identifiers into tickets or logs. +2. Run the read-only preflight queries below and record only aggregate counts. +3. Stop authentication writes for the cutover window. This includes dashboard + auth routes and any job or admin path that can insert or update `account`. +4. Take a database snapshot or an equivalent `pg_dump` of the `account` and + `user` tables. Keep it until the post-deploy checks pass. +5. Add the nullable column and perform the backfill transaction below. +6. Resolve every unmapped provider and every identity collision. Do not + continue while either query returns rows. +7. Set `issuer` to `NOT NULL` and create the unique identity index. +8. Run the postflight checks, then deploy the application and restart all + dashboard instances together. +9. Run the smoke checks below before reopening auth writes. + +## Read-only preflight + +Run these against the intended database with a read-only role first: + +```sql +SELECT provider_id, COUNT(*) AS account_count +FROM account +GROUP BY provider_id +ORDER BY provider_id; + +SELECT COUNT(*) AS account_count, + COUNT(*) FILTER (WHERE provider_id = 'credential') AS credential_count, + COUNT(*) FILTER (WHERE provider_id = 'google') AS google_count, + COUNT(*) FILTER (WHERE provider_id = 'github') AS github_count +FROM account; + +SELECT COUNT(*) AS sso_provider_count +FROM sso_provider; +``` + +The current Databuddy provider map is: + +| `provider_id` | `issuer` | `account_id` treatment | +| --- | --- | --- | +| `credential` | `local:credential` | Replace with the linked `user.id` | +| `google` | `https://accounts.google.com` | Keep the provider subject | +| `github` | `local:oauth:github` | Keep the provider account ID | +| SSO provider IDs | Exact `sso_provider.issuer` | Keep the verified provider subject | + +If preflight finds any provider outside that map, stop and add an explicit +trusted mapping from its Better Auth configuration or provider metadata. Never +derive an issuer from email, display name, request input, or an authorization +URL. + +For SSO, inspect each stored OIDC/SAML mapping before the cutover. Better Auth +1.7 uses the verified OIDC `sub` or signed SAML `NameID`; it no longer treats a +custom `mapping.id` as the account identity. If a stored provider configuration +has a custom identity mapping, stop and obtain a trusted subject mapping from +the identity provider before continuing. Remove the legacy mapping from the +provider configuration in the same controlled change. + +## Additive backfill + +Take the snapshot before this step. The first transaction is safe to retry if it +rolls back, but it must run while authentication writes are stopped. + +```sql +BEGIN; + +ALTER TABLE account ADD COLUMN IF NOT EXISTS issuer text; + +-- Better Auth 1.7 uses the stable local user ID for credential accounts. +UPDATE account AS a +SET issuer = 'local:credential', + account_id = u.id +FROM "user" AS u +WHERE a.provider_id = 'credential' + AND a.user_id = u.id; + +-- Built-in Google uses Google's stable issuer and subject. +UPDATE account +SET issuer = 'https://accounts.google.com' +WHERE provider_id = 'google' + AND issuer IS NULL; + +-- GitHub is OAuth without an issuer in Better Auth's provider definition. +UPDATE account +SET issuer = 'local:oauth:github' +WHERE provider_id = 'github' + AND issuer IS NULL; + +-- SSO account rows use the exact issuer stored with their SSO provider. +UPDATE account AS a +SET issuer = s.issuer +FROM sso_provider AS s +WHERE a.provider_id = s.provider_id + AND a.issuer IS NULL; + +-- Fail closed. An exception rolls back the entire backfill transaction. +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM account WHERE issuer IS NULL OR issuer = '') THEN + RAISE EXCEPTION 'Better Auth 1.7 backfill left unmapped account identities'; + END IF; +END $$; + +COMMIT; +``` + +After the transaction, run the following read-only checks. Do not delete or +merge rows automatically: + +```sql +SELECT provider_id, COUNT(*) AS unmapped_count +FROM account +WHERE issuer IS NULL OR issuer = '' +GROUP BY provider_id; + +SELECT issuer, account_id, + COUNT(*) AS account_count, + COUNT(DISTINCT user_id) AS user_count +FROM account +GROUP BY issuer, account_id +HAVING COUNT(*) > 1 +ORDER BY account_count DESC, issuer, account_id; +``` + +Any collision is a hard stop. If duplicate rows belong to one user, reconcile +tokens, scopes, timestamps, and the provider configuration with the owner +before removing a duplicate. If a key belongs to multiple users, establish the +owner from trusted provider data. Never merge users by matching email alone. + +## Finalize the identity constraint + +Only after the unmapped and collision queries are empty: + +```sql +ALTER TABLE account ALTER COLUMN issuer SET NOT NULL; + +-- Run outside a transaction. The existing provider/account index remains in +-- place for compatibility and is intentionally not dropped in this upgrade. +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS + accounts_issuer_account_unique + ON account (issuer, account_id); +``` + +Verify the definition rather than relying only on the index name: + +```sql +SELECT indexname, indexdef +FROM pg_indexes +WHERE tablename = 'account' + AND indexname IN ( + 'accounts_provider_account_unique', + 'accounts_issuer_account_unique' + ) +ORDER BY indexname; + +SELECT COUNT(*) AS missing_issuer_count +FROM account +WHERE issuer IS NULL; +``` + +The expected `missing_issuer_count` is `0`. If index creation fails, leave the +application on the pre-upgrade version, resolve the reported duplicate keys, +and retry the index step. Do not drop the old provider/account index as a +shortcut. + +## Smoke checks before reopening writes + +Use non-production test accounts and placeholders where possible: + +- Password sign-up, sign-in, password reset, and sign-out. +- Google sign-in and GitHub sign-in; confirm both return to the configured + dashboard origin. +- SSO sign-in for one OIDC/SAML provider, if configured in the environment. +- Account settings: list linked accounts, link a provider, and unlink exactly + the selected account. +- Organization GitHub integration: connect, use the integration, and + disconnect it. +- TOTP enrollment and verification; confirm no OTP enrollment path is used by + the TOTP dialog. +- Magic-link and email-OTP sign-in, including an unconfirmed account, and + confirm the expected cleanup/revocation behavior. +- A GitHub-backed AI tool request with an expired access token, confirming the + refresh path selects the local account row ID. +- Concurrent auth requests against one rate-limit key, confirming the limit + does not allow multiple stale-read passes. + +## Rollback boundary + +The application code is reversible with a normal deployment rollback, but the +credential `account_id` rewrite is an identity migration. Do not attempt a +blind down migration after the new application has created rows. + +Before the new application is deployed, rollback means restoring the captured +`account` table snapshot (or restoring the original `account_id` values by +joining that snapshot on `account.id`), dropping the new index, and dropping +`issuer` only after the old application is serving again. If a post-deploy +rollback is required, restore the whole captured auth-table snapshot in a +maintenance window and deploy the matching old application version. Keep the +snapshot until the new version has passed the smoke checks. + +References: + +- [Better Auth 1.7 upgrade guide](https://better-auth.com/docs/guides/1-7-upgrade-guide) +- [Better Auth 1.7 release overview](https://better-auth.com/blog/1-7) From ad8a0acaf4607842cba0bd4d0b1f893c39d55ec3 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:44:04 +0300 Subject: [PATCH 3/6] chore(auth): remove ad hoc upgrade runbook --- packages/auth/UPGRADE-1.7.md | 234 ----------------------------------- 1 file changed, 234 deletions(-) delete mode 100644 packages/auth/UPGRADE-1.7.md diff --git a/packages/auth/UPGRADE-1.7.md b/packages/auth/UPGRADE-1.7.md deleted file mode 100644 index e655cd81d..000000000 --- a/packages/auth/UPGRADE-1.7.md +++ /dev/null @@ -1,234 +0,0 @@ -# Better Auth 1.7 cutover runbook - -This repository is upgrading from the currently locked Better Auth `1.6.11` -family to `1.7.1`. The application changes in this branch must be deployed only -after the account identity cutover below has completed in the target database. - -This is intentionally an operator-run runbook. The repository currently uses -Drizzle schema push for PostgreSQL and does not have a checked-in PostgreSQL -migration runner. Do not use `bun run db:push` against staging or production for -this change: it cannot choose trusted issuers, migrate credential account IDs, -or stop on identity collisions. - -## What changed in the application - -- All directly used Better Auth packages are pinned to `1.7.1` together. -- `account.issuer` and the `(issuer, account_id)` unique index are represented - in the Drizzle schema. -- The Redis rate-limit adapter now uses Better Auth 1.7's atomic `consume` - contract. The existing Redis Lua sliding-window implementation remains the - counter and continues to fail open on Redis outages as before. -- Account-specific selectors now use the local Better Auth account row ID. - Provider account IDs remain display/token data, not selectors. -- The dashboard explicitly requests TOTP and narrows the new two-factor - response before reading TOTP-only fields. -- The dashboard auth instance has a canonical `baseURL` from the environment- - resolved dashboard URL, so callback and redirect URLs do not depend on a - proxy's forwarded host. -- Databuddy's custom API-key MCP server is not Better Auth MCP and does not - need the `@better-auth/mcp` migration. - -## Required order - -1. Prepare a staging database clone or snapshot. Never use customer data as a - test fixture or paste account identifiers into tickets or logs. -2. Run the read-only preflight queries below and record only aggregate counts. -3. Stop authentication writes for the cutover window. This includes dashboard - auth routes and any job or admin path that can insert or update `account`. -4. Take a database snapshot or an equivalent `pg_dump` of the `account` and - `user` tables. Keep it until the post-deploy checks pass. -5. Add the nullable column and perform the backfill transaction below. -6. Resolve every unmapped provider and every identity collision. Do not - continue while either query returns rows. -7. Set `issuer` to `NOT NULL` and create the unique identity index. -8. Run the postflight checks, then deploy the application and restart all - dashboard instances together. -9. Run the smoke checks below before reopening auth writes. - -## Read-only preflight - -Run these against the intended database with a read-only role first: - -```sql -SELECT provider_id, COUNT(*) AS account_count -FROM account -GROUP BY provider_id -ORDER BY provider_id; - -SELECT COUNT(*) AS account_count, - COUNT(*) FILTER (WHERE provider_id = 'credential') AS credential_count, - COUNT(*) FILTER (WHERE provider_id = 'google') AS google_count, - COUNT(*) FILTER (WHERE provider_id = 'github') AS github_count -FROM account; - -SELECT COUNT(*) AS sso_provider_count -FROM sso_provider; -``` - -The current Databuddy provider map is: - -| `provider_id` | `issuer` | `account_id` treatment | -| --- | --- | --- | -| `credential` | `local:credential` | Replace with the linked `user.id` | -| `google` | `https://accounts.google.com` | Keep the provider subject | -| `github` | `local:oauth:github` | Keep the provider account ID | -| SSO provider IDs | Exact `sso_provider.issuer` | Keep the verified provider subject | - -If preflight finds any provider outside that map, stop and add an explicit -trusted mapping from its Better Auth configuration or provider metadata. Never -derive an issuer from email, display name, request input, or an authorization -URL. - -For SSO, inspect each stored OIDC/SAML mapping before the cutover. Better Auth -1.7 uses the verified OIDC `sub` or signed SAML `NameID`; it no longer treats a -custom `mapping.id` as the account identity. If a stored provider configuration -has a custom identity mapping, stop and obtain a trusted subject mapping from -the identity provider before continuing. Remove the legacy mapping from the -provider configuration in the same controlled change. - -## Additive backfill - -Take the snapshot before this step. The first transaction is safe to retry if it -rolls back, but it must run while authentication writes are stopped. - -```sql -BEGIN; - -ALTER TABLE account ADD COLUMN IF NOT EXISTS issuer text; - --- Better Auth 1.7 uses the stable local user ID for credential accounts. -UPDATE account AS a -SET issuer = 'local:credential', - account_id = u.id -FROM "user" AS u -WHERE a.provider_id = 'credential' - AND a.user_id = u.id; - --- Built-in Google uses Google's stable issuer and subject. -UPDATE account -SET issuer = 'https://accounts.google.com' -WHERE provider_id = 'google' - AND issuer IS NULL; - --- GitHub is OAuth without an issuer in Better Auth's provider definition. -UPDATE account -SET issuer = 'local:oauth:github' -WHERE provider_id = 'github' - AND issuer IS NULL; - --- SSO account rows use the exact issuer stored with their SSO provider. -UPDATE account AS a -SET issuer = s.issuer -FROM sso_provider AS s -WHERE a.provider_id = s.provider_id - AND a.issuer IS NULL; - --- Fail closed. An exception rolls back the entire backfill transaction. -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM account WHERE issuer IS NULL OR issuer = '') THEN - RAISE EXCEPTION 'Better Auth 1.7 backfill left unmapped account identities'; - END IF; -END $$; - -COMMIT; -``` - -After the transaction, run the following read-only checks. Do not delete or -merge rows automatically: - -```sql -SELECT provider_id, COUNT(*) AS unmapped_count -FROM account -WHERE issuer IS NULL OR issuer = '' -GROUP BY provider_id; - -SELECT issuer, account_id, - COUNT(*) AS account_count, - COUNT(DISTINCT user_id) AS user_count -FROM account -GROUP BY issuer, account_id -HAVING COUNT(*) > 1 -ORDER BY account_count DESC, issuer, account_id; -``` - -Any collision is a hard stop. If duplicate rows belong to one user, reconcile -tokens, scopes, timestamps, and the provider configuration with the owner -before removing a duplicate. If a key belongs to multiple users, establish the -owner from trusted provider data. Never merge users by matching email alone. - -## Finalize the identity constraint - -Only after the unmapped and collision queries are empty: - -```sql -ALTER TABLE account ALTER COLUMN issuer SET NOT NULL; - --- Run outside a transaction. The existing provider/account index remains in --- place for compatibility and is intentionally not dropped in this upgrade. -CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS - accounts_issuer_account_unique - ON account (issuer, account_id); -``` - -Verify the definition rather than relying only on the index name: - -```sql -SELECT indexname, indexdef -FROM pg_indexes -WHERE tablename = 'account' - AND indexname IN ( - 'accounts_provider_account_unique', - 'accounts_issuer_account_unique' - ) -ORDER BY indexname; - -SELECT COUNT(*) AS missing_issuer_count -FROM account -WHERE issuer IS NULL; -``` - -The expected `missing_issuer_count` is `0`. If index creation fails, leave the -application on the pre-upgrade version, resolve the reported duplicate keys, -and retry the index step. Do not drop the old provider/account index as a -shortcut. - -## Smoke checks before reopening writes - -Use non-production test accounts and placeholders where possible: - -- Password sign-up, sign-in, password reset, and sign-out. -- Google sign-in and GitHub sign-in; confirm both return to the configured - dashboard origin. -- SSO sign-in for one OIDC/SAML provider, if configured in the environment. -- Account settings: list linked accounts, link a provider, and unlink exactly - the selected account. -- Organization GitHub integration: connect, use the integration, and - disconnect it. -- TOTP enrollment and verification; confirm no OTP enrollment path is used by - the TOTP dialog. -- Magic-link and email-OTP sign-in, including an unconfirmed account, and - confirm the expected cleanup/revocation behavior. -- A GitHub-backed AI tool request with an expired access token, confirming the - refresh path selects the local account row ID. -- Concurrent auth requests against one rate-limit key, confirming the limit - does not allow multiple stale-read passes. - -## Rollback boundary - -The application code is reversible with a normal deployment rollback, but the -credential `account_id` rewrite is an identity migration. Do not attempt a -blind down migration after the new application has created rows. - -Before the new application is deployed, rollback means restoring the captured -`account` table snapshot (or restoring the original `account_id` values by -joining that snapshot on `account.id`), dropping the new index, and dropping -`issuer` only after the old application is serving again. If a post-deploy -rollback is required, restore the whole captured auth-table snapshot in a -maintenance window and deploy the matching old application version. Keep the -snapshot until the new version has passed the smoke checks. - -References: - -- [Better Auth 1.7 upgrade guide](https://better-auth.com/docs/guides/1-7-upgrade-guide) -- [Better Auth 1.7 release overview](https://better-auth.com/blog/1-7) From 17816f26715d5a0b74d3ab98fa23a9188d7c3895 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:39:42 +0300 Subject: [PATCH 4/6] fix(db): complete Better Auth 1.7 auth schema --- packages/db/src/drizzle/schema/auth.test.ts | 17 ++++++++++++++++- packages/db/src/drizzle/schema/auth.ts | 9 +++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/db/src/drizzle/schema/auth.test.ts b/packages/db/src/drizzle/schema/auth.test.ts index 5e77371e9..809b38cbf 100644 --- a/packages/db/src/drizzle/schema/auth.test.ts +++ b/packages/db/src/drizzle/schema/auth.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { getTableConfig } from "drizzle-orm/pg-core"; -import { account } from "./auth"; +import { account, twoFactor } from "./auth"; describe("Better Auth 1.7 account identity schema", () => { test("requires a trusted issuer on every account row", () => { @@ -24,3 +24,18 @@ describe("Better Auth 1.7 account identity schema", () => { ]); }); }); + +describe("Better Auth two-factor schema", () => { + test("supports verified enrollment and account lockout state", () => { + const columns = new Map( + getTableConfig(twoFactor).columns.map((column) => [column.name, column]) + ); + + expect(columns.get("verified")?.notNull).toBe(true); + expect(columns.get("verified")?.default).toBe(true); + expect(columns.get("failed_verification_count")?.notNull).toBe(true); + expect(columns.get("failed_verification_count")?.default).toBe(0); + expect(columns.get("locked_until")?.notNull).toBe(false); + expect(columns.get("locked_until")?.dataType).toBe("object date"); + }); +}); diff --git a/packages/db/src/drizzle/schema/auth.ts b/packages/db/src/drizzle/schema/auth.ts index 44cb8f1a7..4733eeaeb 100644 --- a/packages/db/src/drizzle/schema/auth.ts +++ b/packages/db/src/drizzle/schema/auth.ts @@ -1,6 +1,7 @@ import { boolean, foreignKey, + integer, index, jsonb, pgEnum, @@ -332,6 +333,14 @@ export const twoFactor = pgTable( secret: text().notNull(), backupCodes: text("backup_codes").notNull(), userId: text("user_id").notNull(), + verified: boolean().default(true).notNull(), + failedVerificationCount: integer("failed_verification_count") + .default(0) + .notNull(), + lockedUntil: timestamp("locked_until", { + precision: 3, + withTimezone: true, + }), }, (table) => [ index("idx_two_factor_user_id").using( From d69e42521ef1d7b793e5413adfe58354688e2778 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:59:19 +0300 Subject: [PATCH 5/6] test(rpc): isolate workspace middleware mocks --- .../rpc/src/routers/apikeys.resource-ownership.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rpc/src/routers/apikeys.resource-ownership.test.ts b/packages/rpc/src/routers/apikeys.resource-ownership.test.ts index c811ab578..62ae08602 100644 --- a/packages/rpc/src/routers/apikeys.resource-ownership.test.ts +++ b/packages/rpc/src/routers/apikeys.resource-ownership.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { beforeEach, describe, expect, it, mock } from "bun:test"; import { createProcedureClient } from "@orpc/server"; import { createKeys } from "keypal"; import type { Context } from "../orpc"; @@ -39,6 +39,10 @@ mock.module("../lib/audit", () => ({ const { apikeysRouter } = await import("./apikeys"); +// Keep the router's captured doubles, but do not leak shared module mocks into +// sibling test files that exercise the real workspace middleware. +mock.restore(); + function call(procedure: T, context: Context) { return createProcedureClient(procedure as never, { context }); } @@ -173,7 +177,3 @@ describe("apikeys website resource ownership", () => { expect(mockAppendRpcAuditEvent).toHaveBeenCalledTimes(1); }); }); - -afterAll(() => { - mock.restore(); -}); From 0f86b24d8d0ffee048ae887b6630a25c33ef99c9 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:04:05 +0300 Subject: [PATCH 6/6] test(rpc): isolate package test files --- packages/rpc/package.json | 2 +- .../apikeys.resource-ownership.test.ts | 58 ++++++++++--------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/rpc/package.json b/packages/rpc/package.json index 415a7be0c..6fdfec623 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -7,7 +7,7 @@ "types": "./src/index.ts", "scripts": { "check-types": "tsc --noEmit", - "test": "bun test src/routers src/lib/analytics-utils.integration.test.ts src/procedures/*.test.ts src/services/insight-schedule.test.ts src/services/uptime-lifecycle.test.ts src/services/uptime-scheduler.test.ts src/utils/*.test.ts", + "test": "REDIS_URL=\"${REDIS_URL:-redis://localhost:6379}\" bun test --isolate src/routers src/lib/analytics-utils.integration.test.ts src/procedures/*.test.ts src/services/insight-schedule.test.ts src/services/uptime-lifecycle.test.ts src/services/uptime-scheduler.test.ts src/utils/*.test.ts", "test:integration": "bun test src/services/uptime-scheduler.integration.test.ts" }, "exports": { diff --git a/packages/rpc/src/routers/apikeys.resource-ownership.test.ts b/packages/rpc/src/routers/apikeys.resource-ownership.test.ts index 62ae08602..f78dcc6ac 100644 --- a/packages/rpc/src/routers/apikeys.resource-ownership.test.ts +++ b/packages/rpc/src/routers/apikeys.resource-ownership.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { beforeAll, beforeEach, describe, expect, it, mock } from "bun:test"; import { createProcedureClient } from "@orpc/server"; import { createKeys } from "keypal"; import type { Context } from "../orpc"; @@ -14,34 +14,38 @@ const mockWithWorkspace = mock(async () => ({ })); const mockAppendRpcAuditEvent = mock(async () => undefined); -mock.module("@databuddy/auth", () => ({ - auth: { api: { getSession: async () => null } }, -})); -mock.module("@databuddy/api-keys/resolve", () => ({ - collectScopes: (key: { scopes: string[] }) => key.scopes, - getApiKeyFromHeader: async () => null, - keys: testKeys, - markApiKeyUsed: async () => undefined, - withApiKeyCacheInvalidation: async ( - _hashes: Array, - operation: () => Promise - ) => operation(), -})); -mock.module("../procedures/with-workspace", () => ({ - withWorkspace: mockWithWorkspace, -})); -mock.module("../lib/audit", () => ({ - appendRpcAuditEvent: mockAppendRpcAuditEvent, - getAuditActor: () => ({ id: "user-a", type: "user" }), - getAuditOrganizationId: () => ORGANIZATION_A, - getAuditRequestContext: () => ({}), -})); +let apikeysRouter: typeof import("./apikeys").apikeysRouter; + +beforeAll(async () => { + mock.module("@databuddy/auth", () => ({ + auth: { api: { getSession: async () => null } }, + })); + mock.module("@databuddy/api-keys/resolve", () => ({ + collectScopes: (key: { scopes: string[] }) => key.scopes, + getApiKeyFromHeader: async () => null, + keys: testKeys, + markApiKeyUsed: async () => undefined, + withApiKeyCacheInvalidation: async ( + _hashes: Array, + operation: () => Promise + ) => operation(), + })); + mock.module("../procedures/with-workspace", () => ({ + withWorkspace: mockWithWorkspace, + })); + mock.module("../lib/audit", () => ({ + appendRpcAuditEvent: mockAppendRpcAuditEvent, + getAuditActor: () => ({ id: "user-a", type: "user" }), + getAuditOrganizationId: () => ORGANIZATION_A, + getAuditRequestContext: () => ({}), + })); -const { apikeysRouter } = await import("./apikeys"); + ({ apikeysRouter } = await import("./apikeys")); -// Keep the router's captured doubles, but do not leak shared module mocks into -// sibling test files that exercise the real workspace middleware. -mock.restore(); + // Keep the router's captured doubles, but do not leak shared module mocks into + // sibling test files that exercise the real workspace middleware. + mock.restore(); +}); function call(procedure: T, context: Context) { return createProcedureClient(procedure as never, { context });