224 lines · 9.0 KB
import { env } from "cloudflare:workers";
import { getRequestHeader } from "@tanstack/react-start/server";
import { HttpError } from "./errors";
import { assertVerifiedForWrites } from "./verified-writes";
import { first, run } from "./db";
import { ipBucket } from "~/lib/ip";
import type { Viewer } from "~/lib/types";
import { rateLimitDigest, rateLimitShard } from "./rate-limit-shard";

const HTTP_TOO_MANY = 429;
const HTTP_UNAVAILABLE = 503;

type LimiterName = "AUTH_LIMITER" | "WRITE_LIMITER" | "HEAVY_LIMITER" | "GIT_LIMITER" | "REPORT_LIMITER";

/**
 * Mirrors wrangler.jsonc `ratelimits` (the binding config is not readable at runtime). `failClosed` limiters
 * refuse when the RateLimiter object is unreachable; the rest let traffic through so an outage can't take
 * the forge down (RL-1).
 */
export const RATE_LIMITS: Record<LimiterName, { limit: number; periodSeconds: number; failClosed: boolean }> = {
  AUTH_LIMITER: { limit: 10, periodSeconds: 60, failClosed: true },
  WRITE_LIMITER: { limit: 30, periodSeconds: 60, failClosed: false },
  HEAVY_LIMITER: { limit: 30, periodSeconds: 60, failClosed: false },
  GIT_LIMITER: { limit: 120, periodSeconds: 60, failClosed: false },
  REPORT_LIMITER: { limit: 5, periodSeconds: 60, failClosed: true },
};

/** Per-isolate memo of refusals only: a key known to be over budget skips the RPC until its window ends. */
const refusedUntil = new Map<string, number>();
const REFUSAL_CACHE_MAX = 10_000;

function cachedRefusal(digest: string, now: number): boolean {
  const until = refusedUntil.get(digest);
  if (until === undefined) return false;
  if (until > now) return true;
  refusedUntil.delete(digest);
  return false;
}

function rememberRefusal(digest: string, until: number): void {