diff --git a/src/client/Main.ts b/src/client/Main.ts index cb009ba9d..fec32ca10 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -1013,6 +1013,17 @@ const hideCrazyGamesElements = () => { } }; +// Concurrent renders into #turnstile-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 so only one widget is alive at a time. +// +// Declared above bootstrap() because the boot-time prefetch in +// Client.initialize() runs synchronously when document.readyState !== "loading" +// and would otherwise hit a TDZ on this binding. +let inFlightTurnstileChain: Promise = Promise.resolve(); +const TURNSTILE_TIMEOUT_MS = 20_000; + // Initialize the client when the DOM is loaded const bootstrap = () => { initLayout(); @@ -1036,6 +1047,17 @@ if (document.readyState === "loading") { 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; @@ -1056,15 +1078,33 @@ async function getTurnstileToken(): Promise<{ theme: "light", }); + let cleanedUp = false; + const cleanup = () => { + if (cleanedUp) return; + cleanedUp = true; + try { + window.turnstile.remove(widgetId); + } catch (e) { + console.warn("turnstile.remove failed", e); + } + }; + 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() {