aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/rateLimit.ts
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-07-10 21:22:54 +0000
committerMohamed Bassem <me@mbassem.com>2025-07-10 22:03:30 +0000
commit613137ff99442885c5fe679b2cc1172adfc5a283 (patch)
tree97f2b940448357870090364c6f73b780d6f473d9 /packages/trpc/rateLimit.ts
parent333d1610fad10e70759545f223959503288a02c6 (diff)
downloadkarakeep-613137ff99442885c5fe679b2cc1172adfc5a283.tar.zst
feat: Add API ratelimits
Diffstat (limited to 'packages/trpc/rateLimit.ts')
-rw-r--r--packages/trpc/rateLimit.ts72
1 files changed, 72 insertions, 0 deletions
diff --git a/packages/trpc/rateLimit.ts b/packages/trpc/rateLimit.ts
new file mode 100644
index 00000000..b9aa4aa1
--- /dev/null
+++ b/packages/trpc/rateLimit.ts
@@ -0,0 +1,72 @@
+import { TRPCError } from "@trpc/server";
+
+import serverConfig from "@karakeep/shared/config";
+
+import { Context } from ".";
+
+interface RateLimitConfig {
+ name: string;
+ windowMs: number;
+ maxRequests: number;
+}
+
+interface RateLimitEntry {
+ count: number;
+ resetTime: number;
+}
+
+const rateLimitStore = new Map<string, RateLimitEntry>();
+
+function cleanupExpiredEntries() {
+ const now = Date.now();
+ for (const [key, entry] of rateLimitStore.entries()) {
+ if (now > entry.resetTime) {
+ rateLimitStore.delete(key);
+ }
+ }
+}
+
+setInterval(cleanupExpiredEntries, 60000);
+
+export function createRateLimitMiddleware<T>(config: RateLimitConfig) {
+ return function rateLimitMiddleware(opts: {
+ path: string;
+ ctx: Context;
+ next: () => Promise<T>;
+ }) {
+ if (!serverConfig.rateLimiting.enabled) {
+ return opts.next();
+ }
+ const ip = opts.ctx.req.ip;
+
+ if (!ip) {
+ return opts.next();
+ }
+
+ // TODO: Better fingerprinting
+ const key = `${config.name}:${ip}:${opts.path}`;
+ const now = Date.now();
+
+ let entry = rateLimitStore.get(key);
+
+ if (!entry || now > entry.resetTime) {
+ entry = {
+ count: 1,
+ resetTime: now + config.windowMs,
+ };
+ rateLimitStore.set(key, entry);
+ return opts.next();
+ }
+
+ if (entry.count >= config.maxRequests) {
+ const resetInSeconds = Math.ceil((entry.resetTime - now) / 1000);
+ throw new TRPCError({
+ code: "TOO_MANY_REQUESTS",
+ message: `Rate limit exceeded. Try again in ${resetInSeconds} seconds.`,
+ });
+ }
+
+ entry.count++;
+ return opts.next();
+ };
+}