diff --git a/package-lock.json b/package-lock.json index 1db6a246..664c9982 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@ai-sdk/react": "2.0.100", "@aws-sdk/client-s3": "^3.995.0", "@aws-sdk/s3-request-presigner": "^3.995.0", + "@better-auth/api-key": "1.6.23", "@hookform/resolvers": "^5.2.2", "@langchain/anthropic": "^1.4.0", "@langchain/core": "^1.1.48", @@ -56,7 +57,7 @@ "@trpc/tanstack-react-query": "^11.6.0", "@xyflow/react": "^12.8.6", "ai": "5.0.100", - "better-auth": "1.4.9", + "better-auth": "1.6.23", "class-variance-authority": "^0.7.1", "client-only": "^0.0.1", "clsx": "^2.1.1", @@ -1686,45 +1687,149 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/api-key": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/api-key/-/api-key-1.6.23.tgz", + "integrity": "sha512-HQMTv1GkY5FzE8BlUXdNhuKPBPUvusBYlAtIQElQjfsmGiPiBie7BdzuhcbXhAqJerunU299yvEge2WlZusp+Q==", + "license": "MIT", + "dependencies": { + "zod": "^4.3.6" + }, + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "better-auth": "^1.6.23", + "better-call": "1.3.7" + } + }, "node_modules/@better-auth/core": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.9.tgz", - "integrity": "sha512-JT2q4NDkQzN22KclUEoZ7qU6tl9HUTfK1ctg2oWlT87SEagkwJcnrUwS9VznL+u9ziOIfY27P0f7/jSnmvLcoQ==", + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.23.tgz", + "integrity": "sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ==", + "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "zod": "^4.1.12" + "@opentelemetry/semantic-conventions": "^1.39.0", + "@standard-schema/spec": "^1.1.0", + "zod": "^4.3.6" }, "peerDependencies": { - "@better-auth/utils": "0.3.0", - "@better-fetch/fetch": "1.1.21", - "better-call": "1.1.7", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", + "@cloudflare/workers-types": ">=4", + "@opentelemetry/api": "^1.9.0", + "better-call": "1.3.7", "jose": "^6.1.0", - "kysely": "^0.28.5", + "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } } }, - "node_modules/@better-auth/telemetry": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.4.9.tgz", - "integrity": "sha512-Tthy1/Gmx+pYlbvRQPBTKfVei8+pJwvH1NZp+5SbhwA6K2EXIaoonx/K6N/AXYs2aKUpyR4/gzqDesDjL7zd6A==", - "dependencies": { - "@better-auth/utils": "0.3.0", - "@better-fetch/fetch": "1.1.21" + "node_modules/@better-auth/kysely-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.23.tgz", + "integrity": "sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "kysely": "^0.28.17 || ^0.29.0" }, + "peerDependenciesMeta": { + "kysely": { + "optional": true + } + } + }, + "node_modules/@better-auth/memory-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.23.tgz", + "integrity": "sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ==", + "license": "MIT", "peerDependencies": { - "@better-auth/core": "1.4.9" + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2" + } + }, + "node_modules/@better-auth/mongo-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.23.tgz", + "integrity": "sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "mongodb": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + } + } + }, + "node_modules/@better-auth/prisma-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.23.tgz", + "integrity": "sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@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" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.23.tgz", + "integrity": "sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1" } }, "node_modules/@better-auth/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", - "license": "MIT" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz", + "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1" + } + }, + "node_modules/@better-auth/utils/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@better-fetch/fetch": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.1.tgz", + "integrity": "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==", + "license": "MIT" }, "node_modules/@biomejs/biome": { "version": "2.2.0", @@ -13621,32 +13726,38 @@ } }, "node_modules/better-auth": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.9.tgz", - "integrity": "sha512-usSdjuyTzZwIvM8fjF8YGhPncxV3MAg3dHUO9uPUnf0yklXUSYISiH1+imk6/Z+UBqsscyyPRnbIyjyK97p7YA==", - "license": "MIT", - "dependencies": { - "@better-auth/core": "1.4.9", - "@better-auth/telemetry": "1.4.9", - "@better-auth/utils": "0.3.0", - "@better-fetch/fetch": "1.1.21", - "@noble/ciphers": "^2.0.0", - "@noble/hashes": "^2.0.0", - "better-call": "1.1.7", + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.23.tgz", + "integrity": "sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.6.23", + "@better-auth/drizzle-adapter": "1.6.23", + "@better-auth/kysely-adapter": "1.6.23", + "@better-auth/memory-adapter": "1.6.23", + "@better-auth/mongo-adapter": "1.6.23", + "@better-auth/prisma-adapter": "1.6.23", + "@better-auth/telemetry": "1.6.23", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", + "@noble/ciphers": "^2.1.1", + "@noble/hashes": "^2.0.1", + "better-call": "1.3.7", "defu": "^6.1.4", - "jose": "^6.1.0", - "kysely": "^0.28.5", - "nanostores": "^1.0.1", - "zod": "^4.1.12" + "jose": "^6.1.3", + "kysely": "^0.28.17 || ^0.29.0", + "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.41.0", + "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", @@ -13672,6 +13783,9 @@ "@tanstack/react-start": { "optional": true }, + "@tanstack/solid-start": { + "optional": true + }, "better-sqlite3": { "optional": true }, @@ -13716,6 +13830,22 @@ } } }, + "node_modules/better-auth/node_modules/@better-auth/drizzle-adapter": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.23.tgz", + "integrity": "sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.23", + "@better-auth/utils": "0.4.2", + "drizzle-orm": "^0.45.2" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } + } + }, "node_modules/better-auth/node_modules/@noble/hashes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", @@ -13729,15 +13859,15 @@ } }, "node_modules/better-call": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.7.tgz", - "integrity": "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.7.tgz", + "integrity": "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==", "license": "MIT", "dependencies": { - "@better-auth/utils": "^0.3.0", - "@better-fetch/fetch": "^1.1.4", - "rou3": "^0.7.10", - "set-cookie-parser": "^2.7.1" + "@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" @@ -17177,9 +17307,9 @@ } }, "node_modules/jose": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", - "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -17411,12 +17541,12 @@ } }, "node_modules/kysely": { - "version": "0.28.17", - "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz", - "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.3.tgz", + "integrity": "sha512-VHtBdW6XB/pgoTSqraM3UAa2rYoYdNXqnNPpX+8XXP+cwYbVEFuAp3HyPt1vpNfU9l7Y2kpUrA9QDPsy8uUqOQ==", "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/langsmith": { @@ -18899,9 +19029,9 @@ } }, "node_modules/nanostores": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz", - "integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.4.0.tgz", + "integrity": "sha512-i0tloweeudshAEuddpDxcg9Ik6pkPfVsHIgKyf143JrgG7/MOh0+q7BypdLXZPoOP7fOYt1eTcwGkyiVmhJFkA==", "funding": [ { "type": "github", @@ -21035,9 +21165,9 @@ "license": "MIT" }, "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.1.tgz", + "integrity": "sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==", "license": "MIT" }, "node_modules/setprototypeof": { diff --git a/package.json b/package.json index 376e6627..a4771aa1 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@ai-sdk/react": "2.0.100", "@aws-sdk/client-s3": "^3.995.0", "@aws-sdk/s3-request-presigner": "^3.995.0", + "@better-auth/api-key": "1.6.23", "@hookform/resolvers": "^5.2.2", "@langchain/anthropic": "^1.4.0", "@langchain/core": "^1.1.48", @@ -69,7 +70,7 @@ "@trpc/tanstack-react-query": "^11.6.0", "@xyflow/react": "^12.8.6", "ai": "5.0.100", - "better-auth": "1.4.9", + "better-auth": "1.6.23", "class-variance-authority": "^0.7.1", "client-only": "^0.0.1", "clsx": "^2.1.1", diff --git a/prisma/migrations/20260709120000_apikey_reference_id/migration.sql b/prisma/migrations/20260709120000_apikey_reference_id/migration.sql new file mode 100644 index 00000000..c9c4cc38 --- /dev/null +++ b/prisma/migrations/20260709120000_apikey_reference_id/migration.sql @@ -0,0 +1,15 @@ +-- better-auth's @better-auth/api-key 1.6.x plugin renames the owner column +-- `userId` -> `referenceId` and adds a required `configId`. Rename (not +-- drop/add) so existing keys are preserved. + +-- RenameColumn +ALTER TABLE "apikey" RENAME COLUMN "userId" TO "referenceId"; + +-- RenameForeignKey +ALTER TABLE "apikey" RENAME CONSTRAINT "apikey_userId_fkey" TO "apikey_referenceId_fkey"; + +-- AddColumn +ALTER TABLE "apikey" ADD COLUMN "configId" TEXT NOT NULL DEFAULT 'default'; + +-- CreateIndex +CREATE INDEX "apikey_referenceId_idx" ON "apikey"("referenceId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e9b7c7c3..09f98864 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -676,8 +676,10 @@ model Apikey { start String? prefix String? key String - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + // better-auth's owner column for a key; holds the user's id, so we keep the FK. + referenceId String + configId String @default("default") + user User @relation(fields: [referenceId], references: [id], onDelete: Cascade) refillInterval Int? refillAmount Int? lastRefillAt DateTime? @@ -696,6 +698,7 @@ model Apikey { connector ApiKeyConnector? + @@index([referenceId]) @@map("apikey") } diff --git a/scripts/create-test-api-key.ts b/scripts/create-test-api-key.ts index e262d885..69ea2818 100644 --- a/scripts/create-test-api-key.ts +++ b/scripts/create-test-api-key.ts @@ -21,7 +21,7 @@ async function main() { await prisma.apikey.deleteMany({ where: { - userId: user.id, + referenceId: user.id, name: API_KEY_NAME, }, }); diff --git a/src/features/user/server/routers.ts b/src/features/user/server/routers.ts index 6091f286..9c58cb9e 100644 --- a/src/features/user/server/routers.ts +++ b/src/features/user/server/routers.ts @@ -8,7 +8,6 @@ import { paginationInputSchema, } from "@/lib/pagination"; import { createTRPCRouter, protectedProcedure } from "@/trpc/init"; -import { requireOwnership } from "@/trpc/middleware"; import { apiTokenInputSchema } from "../types"; export const userRouter = createTRPCRouter({ @@ -25,7 +24,7 @@ export const userRouter = createTRPCRouter({ const { search } = input; const whereFilter = { - userId: ctx.auth.user.id, + referenceId: ctx.auth.user.id, name: { contains: search, mode: "insensitive" as const, @@ -92,12 +91,11 @@ export const userRouter = createTRPCRouter({ removeApiToken: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { - // Verify ownership - await requireOwnership(input.id, ctx.auth.user.id, "apikey"); - await prisma.$transaction(async (tx) => { - const apiKeyResult = await tx.apikey.findUniqueOrThrow({ - where: { id: input.id }, + // Scope to the owner (referenceId) so a user can only delete their own + // key; throws if it's missing or not theirs. + const apiKeyResult = await tx.apikey.findFirstOrThrow({ + where: { id: input.id, referenceId: ctx.auth.user.id }, select: { name: true, lastRequest: true, diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts index 19340e06..edd02e3e 100644 --- a/src/lib/auth-client.ts +++ b/src/lib/auth-client.ts @@ -1,4 +1,4 @@ -import { apiKeyClient } from "better-auth/client/plugins"; +import { apiKeyClient } from "@better-auth/api-key/client"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 175adfb0..f6c39146 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,6 @@ +import { apiKey } from "@better-auth/api-key"; import { APIError, betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; -import { apiKey } from "better-auth/plugins"; import { MIN_PASSWORD_LENGTH } from "@/config/constants"; import prisma from "@/lib/db"; import { sendEmail } from "@/lib/mail"; diff --git a/src/trpc/init.ts b/src/trpc/init.ts index 1ad056c8..0ce3aa4c 100644 --- a/src/trpc/init.ts +++ b/src/trpc/init.ts @@ -47,7 +47,9 @@ export const protectedProcedure = baseProcedure.use(async ({ ctx, next }) => { const { valid, error, key } = await verifyApiKey(ctx.req as Request); if (valid && key && !error) { - return next({ ctx: { ...ctx, auth: { user: { id: key.userId }, key } } }); + return next({ + ctx: { ...ctx, auth: { user: { id: key.referenceId }, key } }, + }); } throw new TRPCError({