diff --git a/bun.lock b/bun.lock index ac421cd4..b10104e7 100644 --- a/bun.lock +++ b/bun.lock @@ -135,7 +135,7 @@ "@repo/google-calendar": "workspace:*", "@repo/google-tasks": "workspace:*", "@repo/temporal": "workspace:*", - "@upstash/ratelimit": "^2.0.5", + "@unkey/ratelimit": "^2.1.2", "@upstash/redis": "^1.35.1", }, "devDependencies": { @@ -1462,6 +1462,10 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@unkey/api": ["@unkey/api@2.0.3", "", { "dependencies": { "zod": "^3.20.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.5.0 <1.10.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"], "bin": { "mcp": "bin/mcp-server.js" } }, "sha512-ru+I//qmSTBcZUTV43Pb4LF3JWLgcvXe8/MQKQZV2tA5Ypw61rFwIiOzNM1qKjB4erunFuv7I86/MTCdqfb0eA=="], + + "@unkey/ratelimit": ["@unkey/ratelimit@2.1.2", "", { "dependencies": { "@unkey/api": "2.0.3" } }, "sha512-8zBMmleW5bzwSu9iim0Z5GA1taW2sM+KD3hxQYqemPETwXHqPvghZaMAuPWSfzqda4gLnZSjW0FjUeo4xXWv4A=="], + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], @@ -1500,10 +1504,6 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - "@upstash/core-analytics": ["@upstash/core-analytics@0.0.10", "", { "dependencies": { "@upstash/redis": "^1.28.3" } }, "sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ=="], - - "@upstash/ratelimit": ["@upstash/ratelimit@2.0.5", "", { "dependencies": { "@upstash/core-analytics": "^0.0.10" }, "peerDependencies": { "@upstash/redis": "^1.34.3" } }, "sha512-1FRv0cs3ZlBjCNOCpCmKYmt9BYGIJf0J0R3pucOPE88R21rL7jNjXG+I+rN/BVOvYJhI9niRAS/JaSNjiSICxA=="], - "@upstash/redis": ["@upstash/redis@1.35.1", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-sIMuAMU9IYbE2bkgDby8KLoQKRiBMXn0moXxqLvUmQ7VUu2CvulZLtK8O0x3WQZFvvZhU5sRC2/lOVZdGfudkA=="], "@ver0/deep-equal": ["@ver0/deep-equal@1.0.0", "", {}, "sha512-XKIlF1i6UJiyTL52mDrSDDgRX7Qr5yJ7ts9zn2liZEmhiAEum4XKrJRAWmHdFwCQeGBU+rb+/b0ldw/9V8lOWw=="], @@ -3096,6 +3096,8 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@unkey/api/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-keywords/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], diff --git a/packages/api/package.json b/packages/api/package.json index 95fbd571..5463bd4e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -34,7 +34,7 @@ "@repo/google-calendar": "workspace:*", "@repo/google-tasks": "workspace:*", "@repo/temporal": "workspace:*", - "@upstash/ratelimit": "^2.0.5", + "@unkey/ratelimit": "^2.1.2", "@upstash/redis": "^1.35.1" }, "devDependencies": { diff --git a/packages/api/src/routers/early-access.ts b/packages/api/src/routers/early-access.ts index ea085efc..c9ce4c3a 100644 --- a/packages/api/src/routers/early-access.ts +++ b/packages/api/src/routers/early-access.ts @@ -1,34 +1,11 @@ import { TRPCError } from "@trpc/server"; -import { Ratelimit } from "@upstash/ratelimit"; -import { Redis } from "@upstash/redis"; import { count, eq } from "drizzle-orm"; import { z } from "zod/v3"; import { db } from "@repo/db"; import { waitlist } from "@repo/db/schema"; -import { env } from "@repo/env/server"; import { createTRPCRouter, publicProcedure } from "../trpc"; -import { getIp } from "../utils/ip"; - -let ratelimit: Ratelimit | null = null; - -function getRateLimiter() { - if (!ratelimit) { - const redis = new Redis({ - url: env.UPSTASH_REDIS_REST_URL, - token: env.UPSTASH_REDIS_REST_TOKEN, - }); - - ratelimit = new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(2, "1m"), - analytics: true, - prefix: "ratelimit:early-access-waitlist", - }); - } - return ratelimit; -} export const earlyAccessRouter = createTRPCRouter({ getWaitlistCount: publicProcedure.query(async () => { @@ -46,26 +23,20 @@ export const earlyAccessRouter = createTRPCRouter({ }; }), joinWaitlist: publicProcedure + .meta({ + procedureName: "earlyAccess.joinWaitlist", + ratelimit: { + namespace: "early-access-waitlist", + limit: 2, + duration: "1m", + }, + }) .input( z.object({ email: z.string().email(), }), ) - .mutation(async ({ input, ctx }) => { - // Apply rate limiting if available - const limiter = getRateLimiter(); - if (limiter) { - const ip = getIp(ctx.headers); - const { success } = await limiter.limit(ip); - - if (!success) { - throw new TRPCError({ - code: "TOO_MANY_REQUESTS", - message: "Too many requests. Please try again later.", - }); - } - } - + .mutation(async ({ input }) => { const userAlreadyInWaitlist = await db .select() .from(waitlist) diff --git a/packages/api/src/routers/places.ts b/packages/api/src/routers/places.ts index 7e3d1f41..9176323e 100644 --- a/packages/api/src/routers/places.ts +++ b/packages/api/src/routers/places.ts @@ -1,50 +1,19 @@ -import { TRPCError } from "@trpc/server"; -import { Ratelimit } from "@upstash/ratelimit"; -import { Redis } from "@upstash/redis"; - -import { env } from "@repo/env/server"; - import { GooglePlacesProvider } from "../providers/google-places"; import { autocompleteInputSchema } from "../schemas/places"; import { createTRPCRouter, publicProcedure } from "../trpc"; -import { getIp } from "../utils/ip"; - -let ratelimit: Ratelimit | null = null; - -function getRateLimiter() { - if (!ratelimit) { - const redis = new Redis({ - url: env.UPSTASH_REDIS_REST_URL, - token: env.UPSTASH_REDIS_REST_TOKEN, - }); - - ratelimit = new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(20, "1m"), - analytics: true, - prefix: "ratelimit:google-places", - }); - } - - return ratelimit; -} export const placesRouter = createTRPCRouter({ autocomplete: publicProcedure + .meta({ + procedureName: "places.autocomplete", + ratelimit: { + namespace: "google-places", + limit: 20, + duration: "1m", + }, + }) .input(autocompleteInputSchema) - .query(async ({ input, ctx }) => { - const identifier = getIp(ctx.headers); - const limiter = getRateLimiter(); - - const { success } = await limiter.limit(identifier); - - if (!success) { - throw new TRPCError({ - code: "TOO_MANY_REQUESTS", - message: "Too many requests. Please try again later.", - }); - } - + .query(async ({ input }) => { const placesProvider = new GooglePlacesProvider(); return await placesProvider.autocomplete(input.input, { diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index a6fdf887..d9af285f 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -2,11 +2,12 @@ import "server-only"; import * as Sentry from "@sentry/node"; import { TRPCError, initTRPC } from "@trpc/server"; -import { Ratelimit } from "@upstash/ratelimit"; +import { Ratelimit } from "@unkey/ratelimit"; import { ZodError } from "zod/v3"; import { auth } from "@repo/auth/server"; import { db } from "@repo/db"; +import { env } from "@repo/env/server"; import { getCalendarProvider, @@ -16,7 +17,6 @@ import { } from "./providers"; import { getAccounts } from "./utils/accounts"; import { getIp } from "./utils/ip"; -import { getRedis } from "./utils/redis"; import { superjson } from "./utils/superjson"; type Unit = "ms" | "s" | "m" | "h" | "d"; @@ -77,15 +77,14 @@ export const rateLimitMiddleware = t.middleware(async ({ ctx, meta, next }) => { } const limiter = new Ratelimit({ - redis: getRedis(), - limiter: Ratelimit.slidingWindow( - meta.ratelimit.limit, - meta.ratelimit.duration, - ), + namespace: meta.ratelimit.namespace, + limit: meta.ratelimit.limit, + duration: meta.ratelimit.duration, + rootKey: env.UNKEY_ROOT_KEY, }); - const key = `${meta.ratelimit.namespace}:${meta.procedureName}:${ctx.rateLimit.id}`; - const result = await limiter.limit(key); + const identifier = `${meta.procedureName}:${ctx.rateLimit.id}`; + const result = await limiter.limit(identifier); if (!result.success) { throw new TRPCError({ code: "TOO_MANY_REQUESTS" }); diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index bad093ff..6e6b4515 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -17,6 +17,7 @@ export const env = createEnv({ VERCEL_URL: z.string().optional(), UPSTASH_REDIS_REST_URL: z.string().url(), UPSTASH_REDIS_REST_TOKEN: z.string().min(1), + UNKEY_ROOT_KEY: z.string().min(1), MARBLE_WORKSPACE_KEY: z.string().min(1).optional(), MARBLE_API_URL: z.string().url().optional(), }, diff --git a/turbo.json b/turbo.json index 798cf2ef..edc77dd6 100644 --- a/turbo.json +++ b/turbo.json @@ -16,6 +16,7 @@ "NEXT_PUBLIC_VERCEL_ENV", "UPSTASH_REDIS_REST_URL", "UPSTASH_REDIS_REST_TOKEN", + "UNKEY_ROOT_KEY", "SIMPLE_ANALYTICS_HOSTNAME", "NEXT_PUBLIC_SIMPLE_ANALYTICS_HOSTNAME", "MICROSOFT_CLIENT_ID", @@ -48,6 +49,7 @@ "NEXT_PUBLIC_VERCEL_ENV", "UPSTASH_REDIS_REST_URL", "UPSTASH_REDIS_REST_TOKEN", + "UNKEY_ROOT_KEY", "SIMPLE_ANALYTICS_HOSTNAME", "NEXT_PUBLIC_SIMPLE_ANALYTICS_HOSTNAME", "MICROSOFT_CLIENT_ID",