109 lines · 4.2 KB
import { env } from "cloudflare:workers";
import { first, run } from "../db";
import { hmacSha256Hex, randomId, randomToken, sha256Hex } from "../crypto";
import { HttpError, badRequest } from "../errors";
import { enforceRateLimit, numberVar } from "../limits";
import { DeployError } from "../pages/collect";
import { cookieSecret, labelUrl, SCRATCH_LIMITS } from "./config";
import { activateSiteDeployment, failSiteDeployment, insertDeployment, loadSiteById } from "./deploy";
import { scratchLabel } from "./naming";
import type { CollectedFiles } from "../pages/collect";
import type { SiteRow } from "./config";

const DAY = 24 * 60 * 60 * 1000;
const HTTP_TOO_MANY = 429;
export const CLAIM_PREFIX = "gitfor_claim";

export type ScratchResult = {
  url: string | null;
  claimToken: string;
  claimUrl: string;
  expiresAt: number;
  repo?: string;
};

/** Keyed so stored hashes can't be reversed by enumerating the IPv4 space. */
export function ipHash(ip: string): Promise<string> {
  return hmacSha256Hex(cookieSecret(), `site-ip:${ip}`);
}

/** Rate limit and per-IP daily cap shared by every scratch entry point (tarball, files, git). */
async function assertScratchQuota(ip: string): Promise<string> {
  await enforceRateLimit("HEAVY_LIMITER", `scratch:${ip}`);
  const creator = await ipHash(ip);
  const recent = await first<{ count: number }>(
    "SELECT COUNT(*) AS count FROM sites WHERE creator_ip_hash = ? AND created_at > ?",
    creator,
    Date.now() - DAY,
  );
  const perDay = numberVar(env.SCRATCH_SITES_PER_IP_PER_DAY, SCRATCH_LIMITS.perIpPerDay);
  if ((recent?.count ?? 0) >= perDay) throw new HttpError(HTTP_TOO_MANY, `At most ${perDay} scratch sites per day`);