80 lines · 3.1 KB
import { env } from "cloudflare:workers";
import { forbidden } from "./errors";
import { rawRequestIp } from "./limits";

const SITEVERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const MAX_TOKEN_LENGTH = 2048;
const VERIFY_TIMEOUT_MS = 10_000;

export type TurnstileAction = "signup" | "login";

type SiteverifyResult = { success?: boolean; action?: string; hostname?: string };

function loopbackForge(): boolean {
  try {
    const host = new URL(env.PUBLIC_URL).hostname;
    return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
  } catch {
    return false;
  }
}

/**
 * The public sitekey; Turnstile is on for a deployment exactly when this is set. A loopback forge with no
 * TURNSTILE_SECRET in .dev.vars runs without it; a deployed forge missing the secret fails closed instead.
 */
export function turnstileSiteKey(): string | null {
  const siteKey = env.TURNSTILE_SITEKEY?.trim();
  if (!siteKey) return null;
  if (!env.TURNSTILE_SECRET && loopbackForge()) return null;
  return siteKey;
}

/** TURNSTILE_HOSTNAMES, or this deployment's PUBLIC_URL host, so a production forge never accepts localhost tokens. */
function expectedHostnames(): Set<string> {
  const configured = (env.TURNSTILE_HOSTNAMES ?? "")
    .split(",")
    .map((hostname) => hostname.trim().toLowerCase())
    .filter(Boolean);
  if (configured.length > 0) return new Set(configured);
  try {