132 lines · 5.3 KB
import { env } from "cloudflare:workers";
import { registerLocator } from "../scoped-db/locators";
import { registerSite, siteDb } from "./store";
import { hmacSha256Hex, randomId, randomToken, sha256Hex } from "../crypto";
import { HttpError, badRequest } from "../errors";
import { enforceRateLimit, numberVar } from "../limits";
import { DeployError } from "./collect";
import { cookieSecret, labelUrl, SCRATCH_LIMITS } from "./config";
import { activateSiteDeployment, failSiteDeployment, insertDeployment } from "./deploy";
import { scratchLabel } from "./naming";
import { ipBucket } from "~/lib/ip";
import type { CollectedFiles } from "./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). IPv6 is bucketed to
 * its /64 here too, so a caller passing a raw address can't rotate past the cap.
 */
export async function assertScratchQuota(ip: string): Promise<string> {
  const bucket = ipBucket(ip);
  await enforceRateLimit("HEAVY_LIMITER", `scratch:${bucket}`);
  const creator = await ipHash(bucket);
  // Scratch sites live in separate objects, so the per-IP count is a KV counter per UTC day.