139 lines · 5.4 KB
import { env } from "cloudflare:workers";
import { getRequestHeader } from "@tanstack/react-start/server";
import { HttpError } from "./errors";
import { first, run } from "./db";
import { ipBucket } from "~/lib/ip";
import type { Viewer } from "~/lib/types";

const HTTP_TOO_MANY = 429;

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

/** The exact client address from Cloudflare; use for logs and Turnstile, not for rate-limit keys. */
export function rawRequestIp(request?: Request): string {
  const header = request ? request.headers.get("cf-connecting-ip") : getRequestHeader("cf-connecting-ip");
  return header ?? "unknown";
}

/** The client address as a rate-limit and quota key: IPv6 is bucketed to its /64. */
export function requestIp(request?: Request): string {
  return ipBucket(rawRequestIp(request));
}

/**
 * Local e2e runs only: RATE_LIMITS_DISABLED=true turns the limiters off, and only when PUBLIC_URL is a
 * loopback address, so it can never take effect on a deployed forge.
 */
function rateLimitsDisabled(): boolean {
  if (env.RATE_LIMITS_DISABLED !== "true") return false;
  try {
    const host = new URL(env.PUBLIC_URL).hostname;
    return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
  } catch {
    return false;
  }
}

/** Uses the Workers Rate Limiting binding; throws 429 once the key exceeds its budget. */
export async function enforceRateLimit(limiter: LimiterName, key: string): Promise<void> {
  if (rateLimitsDisabled()) return;
  const { success } = await env[limiter].limit({ key });