From 46266c28febacd1a7f28a22d93a17155ed9a1e07 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 3 May 2026 19:27:32 -0600 Subject: [PATCH] turnstile fix --- src/client/Main.ts | 52 +++++++++++++++++++++++++++++++++++++++++--- src/server/Worker.ts | 4 +--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index cb009ba9d..06d4f9828 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -1033,9 +1033,27 @@ if (document.readyState === "loading") { bootstrap(); } +// Concurrent renders into the same container tangle Cloudflare's widget +// bookkeeping and produce "already executing" + postMessage origin-mismatch +// errors plus malformed tokens that fail siteverify with invalid-input-response. +// Serialize callers and give each their own container element. +let inFlightTurnstileChain: Promise = Promise.resolve(); +const TURNSTILE_TIMEOUT_MS = 20_000; + async function getTurnstileToken(): Promise<{ token: string; createdAt: number; +}> { + const myCall = inFlightTurnstileChain + .catch(() => {}) + .then(() => fetchOneTurnstileToken()); + inFlightTurnstileChain = myCall; + return myCall; +} + +async function fetchOneTurnstileToken(): Promise<{ + token: string; + createdAt: number; }> { // Wait for Turnstile script to load (handles slow connections) let attempts = 0; @@ -1049,7 +1067,28 @@ async function getTurnstileToken(): Promise<{ } const config = await getRuntimeClientServerConfig(); - const widgetId = window.turnstile.render("#turnstile-container", { + + const parent = document.getElementById("turnstile-container"); + if (!parent) { + throw new Error("Missing #turnstile-container element"); + } + const target = document.createElement("div"); + parent.appendChild(target); + + let widgetId: string | undefined; + const cleanup = () => { + if (widgetId !== undefined) { + try { + window.turnstile.remove(widgetId); + } catch (e) { + console.warn("turnstile.remove failed", e); + } + widgetId = undefined; + } + target.remove(); + }; + + widgetId = window.turnstile.render(target, { sitekey: config.turnstileSiteKey(), size: "normal", appearance: "interaction-only", @@ -1057,14 +1096,21 @@ async function getTurnstileToken(): Promise<{ }); return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + cleanup(); + reject(new Error(`Turnstile timed out after ${TURNSTILE_TIMEOUT_MS}ms`)); + }, TURNSTILE_TIMEOUT_MS); + window.turnstile.execute(widgetId, { callback: (token: string) => { - window.turnstile.remove(widgetId); + clearTimeout(timeoutId); + cleanup(); console.log(`Turnstile token received: ${token}`); resolve({ token, createdAt: Date.now() }); }, "error-callback": (errorCode: string) => { - window.turnstile.remove(widgetId); + clearTimeout(timeoutId); + cleanup(); console.error(`Turnstile error: ${errorCode}`); alert(`Turnstile error: ${errorCode}. Please refresh and try again.`); reject(new Error(`Turnstile failed: ${errorCode}`)); diff --git a/src/server/Worker.ts b/src/server/Worker.ts index d8d43433b..2905e1706 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -40,9 +40,7 @@ const workerId = parseInt(process.env.WORKER_ID ?? "0"); const log = logger.child({ comp: `w_${workerId}` }); const playlist = new MapPlaylist(); -// TEMPORARY: Turnstile validation disabled while we diagnose intermittent -// invalid-input-response rejections in v31. Flip back to true to re-enable. -const TURNSTILE_ENABLED = false; +const TURNSTILE_ENABLED = true; // Worker setup export async function startWorker() {