initialcommit

This commit is contained in:
Ryan Barlow
2026-02-04 19:40:49 +00:00
parent 9094b17f6f
commit bf6c206f22
2 changed files with 247 additions and 74 deletions
+7 -74
View File
@@ -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<void> {
async initialise(): Promise<void> {
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}`));
},
});
});
}
+240
View File
@@ -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<TokenData | null> | null = null;
private pollIntervalId: ReturnType<typeof setInterval> | 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<string | null> {
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<TokenData | null> {
// 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<TokenData | null> {
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<TokenData | null> {
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();