import { env } from "cloudflare:workers"; import { getRequestHeader } from "@tanstack/react-start/server"; import { HttpError } from "./errors"; import type { Viewer } from "~/lib/types"; const HTTP_TOO_MANY = 429; type LimiterName = "AUTH_LIMITER" | "WRITE_LIMITER" | "HEAVY_LIMITER" | "GIT_LIMITER"; export function requestIp(request?: Request): string { const header = request ? request.headers.get("cf-connecting-ip") : getRequestHeader("cf-connecting-ip"); return header ?? "unknown"; } /** Uses the Workers Rate Limiting binding; throws 429 once the key exceeds its budget. */ export async function enforceRateLimit(limiter: LimiterName, key: string): Promise { const { success } = await env[limiter].limit({ key }); if (!success) throw new HttpError(HTTP_TOO_MANY, "Rate limit exceeded; slow down and try again shortly"); } /** Rate-limit key for the current caller: the signed-in user, otherwise the client IP. */ export function callerKey(viewer: Viewer | null, request?: Request): string { return viewer ? `user:${viewer.id}` : `ip:${requestIp(request)}`; } export async function limitWrite(viewer: Viewer): Promise { await enforceRateLimit("WRITE_LIMITER", `user:${viewer.id}`); } export async function limitHeavy(viewer: Viewer | null, request?: Request): Promise { await enforceRateLimit("HEAVY_LIMITER", callerKey(viewer, request)); } /** Rejects creation once a per-scope count cap is reached, bounding fan-out work and storage. */ export function assertBelowCap(count: number, cap: number, what: string): void { if (count >= cap) throw new HttpError(HTTP_TOO_MANY, `Limit reached: at most ${cap} ${what}`); } export function numberVar(value: string | undefined, fallback: number): number { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } export const CAPS = { webhooksPerRepo: 20, secretsPerRepo: 100, runnersPerScope: 50, labelsPerRepo: 200, protectionsPerRepo: 50, tokensPerUser: 50, collaboratorsPerRepo: 200, reposPerOwner: 1000, notificationRecipients: 500, activeRunsPerRepo: 20, pendingApprovalsPerRepo: 50, passkeysPerUser: 20, deployTokensPerRepo: 50, reposPerFineGrainedToken: 50, followingPerUser: 2000, threadSubscriptionsPerUser: 5000, environmentsPerRepo: 50, variablesPerScope: 200, schedulesPerRepo: 10, cacheEntriesPerRepo: 1000, } as const; /** Page-load budget for repository browsing; every page costs Artifacts operations. */ export async function limitBrowse(viewer: Viewer | null, request?: Request): Promise { await enforceRateLimit("GIT_LIMITER", `browse:${callerKey(viewer, request)}`); }