From bf6c206f2255b64dd9d32aabb9ddd5ffbe11528f Mon Sep 17 00:00:00 2001 From: Ryan Barlow Date: Wed, 4 Feb 2026 19:40:49 +0000 Subject: [PATCH] initialcommit --- src/client/Main.ts | 81 +---------- src/client/TurnstileManager.ts | 240 +++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 74 deletions(-) create mode 100644 src/client/TurnstileManager.ts diff --git a/src/client/Main.ts b/src/client/Main.ts index ba6569889..2c379ee1d 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -41,6 +41,7 @@ import { SendKickPlayerIntentEvent, SendUpdateGameConfigIntentEvent, } from "./Transport"; +import { turnstileManager } from "./TurnstileManager"; import { UserSettingModal } from "./UserSettingModal"; import "./UsernameInput"; import { genAnonUsername, UsernameInput } from "./UsernameInput"; @@ -162,7 +163,6 @@ declare global { interface Window { GIT_COMMIT: string; INSTANCE_ID: string; - turnstile: any; adsEnabled: boolean; PageOS: { session: { @@ -239,18 +239,12 @@ class Client { private gutterAds: GutterAds; - private turnstileTokenPromise: Promise<{ - token: string; - createdAt: number; - }> | null = null; - constructor() {} - async initialize(): Promise { + async initialise(): Promise { crazyGamesSDK.maybeInit(); - // Prefetch turnstile token so it is available when - // the user joins a lobby. - this.turnstileTokenPromise = getTurnstileToken(); + // Initialise turnstile manager to prefetch and maintain tokens + turnstileManager.initialise(); // Wait for components to render before setting version await customElements.whenDefined("mobile-nav-bar"); @@ -954,29 +948,8 @@ class Client { return null; } - // Always request a new token on crazygames. - if (this.turnstileTokenPromise === null || crazyGamesSDK.isOnCrazyGames()) { - console.log("No prefetched turnstile token, getting new token"); - return (await getTurnstileToken())?.token ?? null; - } - - const token = await this.turnstileTokenPromise; - // Clear promise so a new token is fetched next time - this.turnstileTokenPromise = null; - if (!token) { - console.log("No turnstile token"); - return null; - } - - const tokenTTL = 3 * 60 * 1000; - if (Date.now() < token.createdAt + tokenTTL) { - console.log("Prefetched turnstile token is valid"); - - return token.token; - } else { - console.log("Turnstile token expired, getting new token"); - return (await getTurnstileToken())?.token ?? null; - } + // Use the TurnstileManager to get a token + return turnstileManager.getToken(); } } @@ -992,7 +965,7 @@ const hideCrazyGamesElements = () => { // Initialize the client when the DOM is loaded const bootstrap = () => { initLayout(); - new Client().initialize(); + new Client().initialise(); initNavigation(); // Hide elements immediately @@ -1008,43 +981,3 @@ if (document.readyState === "loading") { } else { bootstrap(); } - -async function getTurnstileToken(): Promise<{ - token: string; - createdAt: number; -}> { - // Wait for Turnstile script to load (handles slow connections) - let attempts = 0; - while (typeof window.turnstile === "undefined" && attempts < 100) { - await new Promise((resolve) => setTimeout(resolve, 100)); - attempts++; - } - - if (typeof window.turnstile === "undefined") { - throw new Error("Failed to load Turnstile script"); - } - - const config = await getServerConfigFromClient(); - const widgetId = window.turnstile.render("#turnstile-container", { - sitekey: config.turnstileSiteKey(), - size: "normal", - appearance: "interaction-only", - theme: "light", - }); - - return new Promise((resolve, reject) => { - window.turnstile.execute(widgetId, { - callback: (token: string) => { - window.turnstile.remove(widgetId); - console.log(`Turnstile token received: ${token}`); - resolve({ token, createdAt: Date.now() }); - }, - "error-callback": (errorCode: string) => { - window.turnstile.remove(widgetId); - console.error(`Turnstile error: ${errorCode}`); - alert(`Turnstile error: ${errorCode}. Please refresh and try again.`); - reject(new Error(`Turnstile failed: ${errorCode}`)); - }, - }); - }); -} diff --git a/src/client/TurnstileManager.ts b/src/client/TurnstileManager.ts new file mode 100644 index 000000000..2a48488c7 --- /dev/null +++ b/src/client/TurnstileManager.ts @@ -0,0 +1,240 @@ +import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; + +declare global { + interface Window { + turnstile: { + render: ( + container: string, + options: { + sitekey: string; + size: string; + appearance: string; + theme: string; + }, + ) => string; + execute: ( + widgetId: string, + callbacks: { + callback: (token: string) => void; + "error-callback": (errorCode: string) => void; + }, + ) => void; + remove: (widgetId: string) => void; + }; + } +} + +interface TokenData { + token: string; + createdAt: number; +} + +type ManagerState = "idle" | "fetching" | "ready"; + +const TOKEN_TTL_MS = 3 * 60 * 1000; +const POLL_INTERVAL_MS = 30 * 1000; +const TOKEN_REFRESH_BUFFER_MS = 30 * 1000; // Refresh 30 seconds before expiry + +class TurnstileManager { + private state: ManagerState = "idle"; + private currentToken: TokenData | null = null; + private pendingPromise: Promise | null = null; + private pollIntervalId: ReturnType | null = null; + + initialise(): void { + console.log("Turnstile initialising"); + this.ensureTokenAvailable(); + this.startPolling(); + } + + destroy(): void { + this.stopPolling(); + } + + /** + * Get a token for use. This will: + * - Return the cached token if valid + * - Wait for a pending request if one is in progress + * - Start a new request if needed + */ + async getToken(): Promise { + const tokenData = await this.acquireToken(); + if (!tokenData) { + return null; + } + + // Check if token is still valid + if (!this.isTokenValid(tokenData)) { + console.log("TurnstileManager acquired token is expired, fetching new"); + // Token expired during wait, need to fetch fresh + this.currentToken = null; + this.state = "idle"; + const freshToken = await this.acquireToken(); + if (freshToken) { + this.scheduleRefresh(); + return freshToken.token; + } + return null; + } + + // Mark token as consumed and trigger background refresh + this.scheduleRefresh(); + + return tokenData.token; + } + + hasValidToken(): boolean { + return this.currentToken !== null && this.isTokenValid(this.currentToken); + } + + isFetching(): boolean { + return this.state === "fetching"; + } + + private async acquireToken(): Promise { + // If we have a valid cached token, return it + if (this.currentToken && this.isTokenValid(this.currentToken)) { + console.log("TurnstileManager using cached valid token"); + return this.currentToken; + } + + // If a fetch is already in progress, wait for it + if (this.state === "fetching" && this.pendingPromise) { + console.log("TurnstileManager waiting for pending token request"); + return this.pendingPromise; + } + + // Need to fetch a new token + return this.fetchNewToken(); + } + + private async fetchNewToken(): Promise { + console.log("TurnstileManager starting new token fetch"); + this.state = "fetching"; + + this.pendingPromise = this.doFetchToken(); + + try { + const tokenData = await this.pendingPromise; + this.currentToken = tokenData; + this.state = tokenData ? "ready" : "idle"; + console.log( + `TurnstileManager token fetch complete, state: ${this.state}`, + ); + return tokenData; + } catch (error) { + console.error("TurnstileManager token fetch failed:", error); + this.state = "idle"; + this.currentToken = null; + return null; + } finally { + this.pendingPromise = null; + } + } + + private async doFetchToken(): Promise { + try { + // Wait for Turnstile script to load + let attempts = 0; + while (typeof window.turnstile === "undefined" && attempts < 100) { + await new Promise((resolve) => setTimeout(resolve, 100)); + attempts++; + } + + if (typeof window.turnstile === "undefined") { + console.error("TurnstileManager Turnstile script failed to load"); + return null; + } + + const config = await getServerConfigFromClient(); + const widgetId = window.turnstile.render("#turnstile-container", { + sitekey: config.turnstileSiteKey(), + size: "normal", + appearance: "interaction-only", + theme: "light", + }); + + return new Promise((resolve) => { + window.turnstile.execute(widgetId, { + callback: (token: string) => { + window.turnstile.remove(widgetId); + console.log("TurnstileManager token received"); + resolve({ token, createdAt: Date.now() }); + }, + "error-callback": (errorCode: string) => { + window.turnstile.remove(widgetId); + console.error(`TurnstileManager Turnstile error: ${errorCode}`); + resolve(null); + }, + }); + }); + } catch (error) { + console.error("TurnstileManager Error in doFetchToken:", error); + return null; + } + } + + private isTokenValid(tokenData: TokenData): boolean { + return Date.now() < tokenData.createdAt + TOKEN_TTL_MS; + } + + private isTokenNearExpiry(tokenData: TokenData): boolean { + return ( + Date.now() > tokenData.createdAt + TOKEN_TTL_MS - TOKEN_REFRESH_BUFFER_MS + ); + } + + private scheduleRefresh(): void { + // Clear current token since it was just used + this.currentToken = null; + this.state = "idle"; + + // Start fetching a new one in the background (don't await) + console.log("TurnstileManager scheduling background token refresh"); + this.ensureTokenAvailable(); + } + + /** + * Ensure we have a token available (or are fetching one) + */ + private ensureTokenAvailable(): void { + if (this.state === "fetching") { + // Already fetching, nothing to do + return; + } + + if (this.currentToken && this.isTokenValid(this.currentToken)) { + // Check if token is near expiry and refetch + if (this.isTokenNearExpiry(this.currentToken)) { + console.log("TurnstileManager token near expiry, refetching"); + this.fetchNewToken(); + } + return; + } + + // No valid token, start fetching + this.fetchNewToken(); + } + + private startPolling(): void { + if (this.pollIntervalId) { + return; + } + + this.pollIntervalId = setInterval(() => { + this.ensureTokenAvailable(); + }, POLL_INTERVAL_MS); + + console.log("TurnstileManager polling started"); + } + + private stopPolling(): void { + if (this.pollIntervalId) { + clearInterval(this.pollIntervalId); + this.pollIntervalId = null; + console.log("TurnstileManager polling stopped"); + } + } +} + +export const turnstileManager = new TurnstileManager();