import { io } from "socket.io-client"; import * as fs from "fs"; import * as path from "path"; const PORT = process.env.PORT || 3000; const FOUNDRY_URL = process.env.FOUNDRY_URL || "http://localhost:30000"; function getFoundryUrl(subpath: string): string { const base = FOUNDRY_URL.endsWith("/") ? FOUNDRY_URL.slice(0, -1) : FOUNDRY_URL; const normalizedPath = subpath.startsWith("/") ? subpath : `/${subpath}`; return `${base}${normalizedPath}`; } const SHARED_DIR = "/app/shared"; const ACTORS_FILE = path.join(SHARED_DIR, "ACTORS.DAT"); const UPDATES_FILE = path.join(SHARED_DIR, "UPDATES.DAT"); const WORLDS_CACHE_FILE = path.join(SHARED_DIR, "WORLDS_CACHE.json"); const FOUNDRY_ADMIN_KEY = process.env.FOUNDRY_ADMIN_KEY || ""; let currentWorld = process.env.FOUNDRY_WORLD || ""; let currentWorldTitle = ""; let currentSystem = ""; let currentUser = process.env.FOUNDRY_USER || "Admin"; let currentPassword = process.env.FOUNDRY_PASSWORD || ""; let socket: any = null; let actorsList: any[] = []; interface SystemMapping { getHP: (actor: any) => { value: number; max: number }; setHP: (hp: { value: number; max: number }, actorSystem: any) => void; getLevel: (actor: any) => number; setLevel: (level: number, actorSystem: any) => void; } const SYSTEM_MAPPINGS: Record = { "hbm-rpg-v3": { getHP: (actor) => { const h = actor.system?.attributes?.health || actor.system?.attributes?.hp || {}; return { value: h.value ?? 0, max: h.max ?? 0 }; }, setHP: (hp, actorSystem) => { actorSystem.attributes = actorSystem.attributes || {}; actorSystem.attributes.health = { value: hp.value, max: hp.max }; actorSystem.attributes.hp = { value: hp.value, max: hp.max }; }, getLevel: (actor) => { return actor.system?.details?.year ?? actor.system?.details?.level ?? 0; }, setLevel: (level, actorSystem) => { actorSystem.details = actorSystem.details || {}; actorSystem.details.year = level; actorSystem.details.level = level; } }, "dnd5e": { getHP: (actor) => { const hp = actor.system?.attributes?.hp || {}; return { value: hp.value ?? 0, max: hp.max ?? 0 }; }, setHP: (hp, actorSystem) => { actorSystem.attributes = actorSystem.attributes || {}; actorSystem.attributes.hp = { value: hp.value, max: hp.max }; }, getLevel: (actor) => { return actor.system?.details?.level ?? 0; }, setLevel: (level, actorSystem) => { actorSystem.details = actorSystem.details || {}; actorSystem.details.level = level; } }, "pf2e": { getHP: (actor) => { const hp = actor.system?.attributes?.hp || {}; return { value: hp.value ?? 0, max: hp.max ?? 0 }; }, setHP: (hp, actorSystem) => { actorSystem.attributes = actorSystem.attributes || {}; actorSystem.attributes.hp = { value: hp.value, max: hp.max }; }, getLevel: (actor) => { return actor.system?.details?.level?.value ?? actor.system?.details?.level ?? 0; }, setLevel: (level, actorSystem) => { actorSystem.details = actorSystem.details || {}; actorSystem.details.level = actorSystem.details.level || {}; if (typeof actorSystem.details.level === 'object') { actorSystem.details.level.value = level; } else { actorSystem.details.level = level; } } } }; function getActiveMapping(): SystemMapping { return SYSTEM_MAPPINGS[currentSystem] || SYSTEM_MAPPINGS["dnd5e"]; } if (!fs.existsSync(SHARED_DIR)) { fs.mkdirSync(SHARED_DIR, { recursive: true }); } function disconnectFromFoundry() { if (socket) { console.log("Disconnecting existing WebSocket from Foundry VTT..."); socket.disconnect(); socket = null; } actorsList = []; writeActorsToDat(); } async function fetchUserId(cookie: string): Promise { return new Promise((resolve, reject) => { const tempSocket = io(getFoundryUrl("/"), { forceNew: true, multiplex: false, transports: ["websocket", "polling"], extraHeaders: { Cookie: cookie }, timeout: 5000 }); tempSocket.on("connect", () => { tempSocket.emit("getJoinData", (data: any) => { tempSocket.disconnect(); console.log("getJoinData response:", JSON.stringify(data)); if (data && data.users) { if (data.world) { currentWorldTitle = data.world.title || ""; currentSystem = data.world.system || ""; } const user = data.users.find((u: any) => u.name.toLowerCase() === currentUser.toLowerCase()); if (user) { resolve(user._id || user.id); } else { reject(new Error(`User "${currentUser}" not found in world users list. Available: ${data.users.map((u: any) => u.name).join(", ")}`)); } } else { reject(new Error(`Failed to retrieve users list. Data: ${data ? JSON.stringify(data) : "null"}`)); } }); }); tempSocket.on("connect_error", (err) => { tempSocket.disconnect(); reject(err); }); }); } async function connectToFoundry() { disconnectFromFoundry(); if (!currentWorld) { console.log("No active world configured. Waiting for world selection... "); return; } console.log(`Connecting to Foundry VTT world "${currentWorld}" as "${currentUser}" at ${FOUNDRY_URL}...`); try { const joinPageRes = await fetch(getFoundryUrl("/join")); if (joinPageRes.url.includes("/setup")) { console.log(`World "${currentWorld}" is not running. Please launch it first.`); return; } const cookies = joinPageRes.headers.getSetCookie(); console.log("Response cookies from join page:", cookies); let guestCookie = ""; for (const c of cookies) { if (c.startsWith("session=")) { guestCookie = c.split(";")[0]; break; } } console.log("Extracted guest cookie:", guestCookie); if (!guestCookie) { throw new Error("Failed to obtain guest session cookie from join page."); } console.log("Fetching User ID from join socket..."); const userId = await fetchUserId(guestCookie); console.log(`Found User ID for "${currentUser}": ${userId}`); const postData = { userid: userId, password: currentPassword, action: "join" }; console.log(`Sending POST /join with JSON payload...`); const loginRes = await fetch(getFoundryUrl("/join"), { method: "POST", headers: { "Content-Type": "application/json", Cookie: guestCookie }, body: JSON.stringify(postData), redirect: "manual" }); console.log(`Login response status: ${loginRes.status}`); const setCookieHeaders = loginRes.headers.getSetCookie(); console.log("Login response set-cookie headers:", setCookieHeaders); let sessionCookie = ""; for (const cookie of setCookieHeaders) { if (cookie.startsWith("session=")) { sessionCookie = cookie.split(";")[0]; break; } } if (!sessionCookie) { if (loginRes.status === 200) { console.log("No new session cookie returned, using guest cookie as session cookie."); sessionCookie = guestCookie; } else { const htmlText = await loginRes.text(); console.log(`Login failed. Response body length: ${htmlText.length}`); // Log some body context if we can find error messages if (htmlText.includes("Invalid password") || htmlText.includes("error")) { console.log("Found error indicator in body. HTML preview:", htmlText.substring(0, 1000)); } else { console.log("HTML preview:", htmlText.substring(0, 500)); } throw new Error(`Failed to get session cookie from login (Status: ${loginRes.status}).`); } } console.log("Authenticated successfully! Connecting WebSocket..."); socket = io(getFoundryUrl("/"), { forceNew: true, multiplex: false, transports: ["websocket", "polling"], extraHeaders: { Cookie: sessionCookie } }); socket.on("connect", () => { console.log(`WebSocket connected to Foundry VTT world "${currentWorld}"!`); fetchActors().catch(err => console.error("Error fetching actors:", err)); }); socket.on("disconnect", () => { console.log("WebSocket disconnected."); }); socket.on("createActor", (actor: any) => { console.log(`Actor created in Foundry: ${actor._id || actor.id}`); fetchActors().catch(err => console.error("Error fetching actors on create:", err)); }); socket.on("updateActor", (actor: any) => { console.log(`Actor updated in Foundry: ${actor._id || actor.id}`); fetchActors().catch(err => console.error("Error fetching actors on update:", err)); }); socket.on("deleteActor", (actor: any) => { console.log(`Actor deleted in Foundry: ${actor._id || actor.id}`); fetchActors().catch(err => console.error("Error fetching actors on delete:", err)); }); } catch (err: any) { console.error("Error connecting to Foundry:", err.message); console.log("Retrying in 10s..."); setTimeout(connectToFoundry, 10000); } } function fetchActors(): Promise { return new Promise((resolve, reject) => { if (!socket) { reject(new Error("WebSocket offline")); return; } const req = { type: "Actor", action: "get", operation: { query: {} } }; socket.emit("modifyDocument", req, (response: any) => { if (response && response.result) { const mapping = getActiveMapping(); actorsList = response.result.map((actor: any) => { if (actor.system) { const hp = mapping.getHP(actor); const level = mapping.getLevel(actor); actor.system.attributes = actor.system.attributes || {}; actor.system.attributes.hp = hp; actor.system.details = actor.system.details || {}; actor.system.details.level = level; } return actor; }); console.log(`Fetched ${actorsList.length} actors.`); writeActorsToDat(); resolve(actorsList); } else { console.log("Failed to fetch actors. Response:", JSON.stringify(response)); reject(new Error("Failed to fetch actors")); } }); }); } function writeActorsToDat() { try { const mapping = getActiveMapping(); const lines = actorsList.map(actor => { const id = (actor._id || "").padEnd(16).substring(0, 16); const name = (actor.name || "").padEnd(20).substring(0, 20); const type = (actor.type || "").padEnd(10).substring(0, 10); const hp = mapping.getHP(actor); const hpCur = String(hp.value).padStart(3, "0").substring(0, 3); const hpMax = String(hp.max).padStart(3, "0").substring(0, 3); const lvlVal = mapping.getLevel(actor); const level = String(lvlVal).padStart(2, "0").substring(0, 2); const padding = "".padEnd(25); return `${id}${name}${type}${hpCur}${hpMax}${level}${padding}`; }); fs.writeFileSync(ACTORS_FILE, lines.join("\n") + "\n", "utf8"); console.log(`Synced actors to ${ACTORS_FILE}`); } catch (err) { console.error("Error writing ACTORS.DAT:", err); } } function startWatchingCobolUpdates() { console.log(`Watching ${UPDATES_FILE}...`); if (!fs.existsSync(UPDATES_FILE)) { fs.writeFileSync(UPDATES_FILE, "", "utf8"); } let fsWait = false; fs.watch(UPDATES_FILE, (eventType) => { if (eventType === "change") { if (fsWait) return; fsWait = true; setTimeout(() => { fsWait = false; processCobolUpdates(); }, 200); } }); } function processCobolUpdates() { try { if (!fs.existsSync(UPDATES_FILE)) return; const content = fs.readFileSync(UPDATES_FILE, "utf8"); if (!content.trim()) return; const lines = content.split("\n"); const updatesToSend: any[] = []; for (const line of lines) { if (line.length < 54) continue; const id = line.substring(0, 16).trim(); const name = line.substring(16, 36).trim(); const type = line.substring(36, 46).trim(); const hpCur = parseInt(line.substring(46, 49), 10); const hpMax = parseInt(line.substring(49, 52), 10); const level = parseInt(line.substring(52, 54), 10); if (!id) continue; const mapping = getActiveMapping(); const actorSystem: any = {}; mapping.setHP({ value: hpCur, max: hpMax }, actorSystem); mapping.setLevel(level, actorSystem); const update: any = { _id: id, name: name, system: actorSystem }; updatesToSend.push(update); } if (updatesToSend.length > 0 && socket) { console.log(`Sending updates from COBOL updates to Foundry...`); const req = { type: "Actor", action: "update", operation: { updates: updatesToSend } }; socket.emit("modifyDocument", req, async (res: any) => { console.log("Foundry update response received."); try { await fetchActors(); } catch (err) { console.error("Error refetching actors after COBOL update:", err); } fs.writeFileSync(UPDATES_FILE, "", "utf8"); }); } } catch (err) { console.error("Error processing COBOL updates:", err); } } async function getAdminSessionCookie(): Promise { if (!FOUNDRY_ADMIN_KEY) { throw new Error("FOUNDRY_ADMIN_KEY is not configured in backend environment."); } const res = await fetch(getFoundryUrl("/setup"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: "adminPassword", adminPassword: FOUNDRY_ADMIN_KEY }), redirect: "manual", }); if (res.status === 403) { throw new Error("Invalid Administrator Access Key (check FOUNDRY_ADMIN_KEY in .env.backend)."); } const cookies = res.headers.getSetCookie(); let sessionCookie = ""; for (const cookie of cookies) { if (cookie.startsWith("session=")) { sessionCookie = cookie.split(";")[0]; break; } } if (!sessionCookie) { throw new Error("Failed to get session cookie from setup login."); } return sessionCookie; } function readWorldsCache(): any[] { try { if (fs.existsSync(WORLDS_CACHE_FILE)) { const content = fs.readFileSync(WORLDS_CACHE_FILE, "utf8"); return JSON.parse(content); } } catch (err) { console.error("Error reading worlds cache:", err); } return []; } function writeWorldsCache(worlds: any[]) { try { fs.writeFileSync(WORLDS_CACHE_FILE, JSON.stringify(worlds, null, 2), "utf8"); console.log("Worlds list cached successfully."); } catch (err) { console.error("Error writing worlds cache:", err); } } async function getActiveWorldFromJoin(): Promise<{ id: string; title: string } | null> { try { const joinPageRes = await fetch(getFoundryUrl("/join")); if (joinPageRes.url.includes("/setup")) { return null; } const cookies = joinPageRes.headers.getSetCookie(); let guestCookie = ""; for (const c of cookies) { if (c.startsWith("session=")) { guestCookie = c.split(";")[0]; break; } } if (!guestCookie) return null; return new Promise((resolve) => { const tempSocket = io(getFoundryUrl("/"), { forceNew: true, multiplex: false, transports: ["websocket", "polling"], extraHeaders: { Cookie: guestCookie }, timeout: 4000 }); tempSocket.on("connect", () => { tempSocket.emit("getJoinData", (data: any) => { tempSocket.disconnect(); if (data && data.world) { resolve({ id: data.world.id, title: data.world.title, system: data.world.system }); } else { resolve(null); } }); }); tempSocket.on("connect_error", () => { tempSocket.disconnect(); resolve(null); }); }); } catch (err) { console.error("Error in getActiveWorldFromJoin:", err); return null; } } async function handleWorldsListFallback(originalError: any): Promise { const activeWorld = await getActiveWorldFromJoin(); if (activeWorld) { console.log(`handleWorldsListFallback: Detected active world "${activeWorld.id}" from join page.`); currentWorldTitle = activeWorld.title; currentSystem = activeWorld.system || ""; const cached = readWorldsCache(); if (cached && cached.length > 0) { if (!cached.find((w: any) => w.id === activeWorld.id)) { cached.push({ id: activeWorld.id, title: activeWorld.title }); writeWorldsCache(cached); } return cached.map((w: any) => ({ id: w.id, title: w.title, active: w.id === activeWorld.id })); } else { const singleWorldList = [{ id: activeWorld.id, title: activeWorld.title, active: true }]; writeWorldsCache([{ id: activeWorld.id, title: activeWorld.title }]); return singleWorldList; } } else { const cached = readWorldsCache(); if (cached && cached.length > 0) { console.log("handleWorldsListFallback: No active world detected, returning cached worlds list with active: false."); return cached.map((w: any) => ({ id: w.id, title: w.title, active: false })); } throw originalError || new Error("No worlds data returned from setup socket"); } } async function fetchWorldsList(): Promise { console.log("fetchWorldsList: Authenticating and getting setup cookie..."); let cookie: string; try { cookie = await getAdminSessionCookie(); } catch (err: any) { console.error("fetchWorldsList: Failed to get admin session cookie:", err.message); return await handleWorldsListFallback(err); } console.log("fetchWorldsList: Cookie retrieved successfully. Connecting setup socket..."); try { const worlds = await new Promise((resolve, reject) => { const socketSetup = io(getFoundryUrl("/"), { forceNew: true, multiplex: false, transports: ["websocket", "polling"], extraHeaders: { Cookie: cookie }, timeout: 5000 }); socketSetup.on("connect", () => { console.log("fetchWorldsList: Setup socket connected. Emitting getSetupData..."); socketSetup.emit("getSetupData", (data: any) => { socketSetup.disconnect(); if (data && data.worlds) { const activeWorldId = data.options?.world || null; console.log(`fetchWorldsList: Found ${data.worlds.length} worlds. Active world ID: ${activeWorldId}`); const worldsList = data.worlds.map((w: any) => ({ id: w.id, title: w.title, active: w.id === activeWorldId })); const activeWorld = worldsList.find(w => w.active); if (activeWorld) { currentWorldTitle = activeWorld.title; } writeWorldsCache(worldsList.map((w: any) => ({ id: w.id, title: w.title }))); resolve(worldsList); } else { console.log("fetchWorldsList: No worlds data found in response. Full data keys:", data ? Object.keys(data) : "null"); reject(new Error("No worlds data returned from setup socket")); } }); }); socketSetup.on("connect_error", (err) => { console.error("fetchWorldsList: Setup socket connection error:", err.message); socketSetup.disconnect(); reject(err); }); }); return worlds; } catch (err) { console.log("fetchWorldsList: Setup socket failed or returned empty. Attempting fallback..."); return await handleWorldsListFallback(err); } } async function getActiveGMCookie(): Promise { const joinPageRes = await fetch(getFoundryUrl("/join")); if (joinPageRes.url.includes("/setup")) { return ""; } const cookies = joinPageRes.headers.getSetCookie(); let guestCookie = ""; for (const c of cookies) { if (c.startsWith("session=")) { guestCookie = c.split(";")[0]; break; } } if (!guestCookie) { throw new Error("Failed to get guest session cookie for GM login."); } const userId = await fetchUserId(guestCookie); const postData = { userid: userId, password: currentPassword, action: "join" }; const loginRes = await fetch(getFoundryUrl("/join"), { method: "POST", headers: { "Content-Type": "application/json", Cookie: guestCookie }, body: JSON.stringify(postData), redirect: "manual" }); const setCookieHeaders = loginRes.headers.getSetCookie(); let sessionCookie = ""; for (const cookie of setCookieHeaders) { if (cookie.startsWith("session=")) { sessionCookie = cookie.split(";")[0]; break; } } if (!sessionCookie) { if (loginRes.status === 200) { console.log("No new session cookie returned for GM, using guest cookie as session cookie."); sessionCookie = guestCookie; } else { console.error(`GM login failed with status ${loginRes.status}`); } } return sessionCookie; } async function launchWorld(worldId: string) { const cookie = await getAdminSessionCookie(); const res = await fetch(getFoundryUrl("/setup"), { method: "POST", headers: { "Content-Type": "application/json", Cookie: cookie, }, body: JSON.stringify({ action: "launchWorld", world: worldId }), }); if (!res.ok) { throw new Error(`Failed to launch world ${worldId}`); } } async function shutdownWorld() { console.log("Shutting down active world..."); let cookie = ""; try { cookie = await getActiveGMCookie(); } catch (err: any) { console.log("Could not get GM cookie for shutdown (setup might be active):", err.message); } if (!cookie) { console.log("Attempting shutdown using Admin access..."); cookie = await getAdminSessionCookie(); } const res = await fetch(getFoundryUrl("/setup"), { method: "POST", headers: { "Content-Type": "application/json", Cookie: cookie, }, body: JSON.stringify({ shutdown: true }), }); if (!res.ok) { throw new Error(`Failed to shut down active world. Status: ${res.status}`); } console.log("World shutdown successfully completed."); } Bun.serve({ port: PORT, async fetch(req) { const url = new URL(req.url); const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Content-Type": "application/json" }; if (req.method === "OPTIONS") { return new Response(null, { headers }); } if (url.pathname === "/api/actors" && req.method === "GET") { return new Response(JSON.stringify(actorsList), { headers }); } if (url.pathname === "/api/actors/refetch" && req.method === "POST") { try { if (socket) { const list = await fetchActors(); return new Response(JSON.stringify(list), { headers }); } else { return new Response(JSON.stringify({ error: "WebSocket offline" }), { status: 503, headers }); } } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } } if (url.pathname === "/api/actors/update" && req.method === "POST") { try { const body: any = await req.json(); const { id, updates } = body; if (!id || !updates) { return new Response(JSON.stringify({ error: "Missing parameters" }), { status: 400, headers }); } if (socket) { if (updates.system) { const mapping = getActiveMapping(); const hpVal = updates.system.attributes?.hp?.value; const hpMax = updates.system.attributes?.hp?.max; const level = updates.system.details?.level; const actorSystem: any = {}; if (hpVal !== undefined && hpMax !== undefined) { mapping.setHP({ value: hpVal, max: hpMax }, actorSystem); } if (level !== undefined) { mapping.setLevel(level, actorSystem); } updates.system = actorSystem; } const reqObj = { type: "Actor", action: "update", operation: { updates: [{ _id: id, ...updates }] } }; await new Promise((resolve, reject) => { socket.emit("modifyDocument", reqObj, async (res: any) => { console.log(`Updated Actor ${id} via REST API. Response:`, JSON.stringify(res)); try { await fetchActors(); resolve(); } catch (err) { reject(err); } }); }); return new Response(JSON.stringify({ success: true }), { headers }); } else { return new Response(JSON.stringify({ error: "WebSocket offline" }), { status: 503, headers }); } } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } } if (url.pathname === "/api/worlds" && req.method === "GET") { try { const worlds = await fetchWorldsList(); return new Response(JSON.stringify(worlds), { headers }); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } } if (url.pathname === "/api/worlds/launch" && req.method === "POST") { try { const { worldId } = await req.json(); if (!worldId) { return new Response(JSON.stringify({ error: "Missing worldId" }), { status: 400, headers }); } // Find if there's currently an active world const worlds = await fetchWorldsList(); const activeWorld = worlds.find(w => w.active); if (activeWorld) { console.log(`Shutting down active world ${activeWorld.id} first...`); await shutdownWorld(); await new Promise(r => setTimeout(r, 2000)); } console.log(`Launching world ${worldId}...`); await launchWorld(worldId); await new Promise(r => setTimeout(r, 3000)); currentWorld = worldId; connectToFoundry(); return new Response(JSON.stringify({ success: true }), { headers }); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } } if (url.pathname === "/api/worlds/shutdown" && req.method === "POST") { try { console.log("Shutting down active world..."); await shutdownWorld(); disconnectFromFoundry(); currentWorld = ""; return new Response(JSON.stringify({ success: true }), { headers }); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } } if (url.pathname === "/api/worlds/select" && req.method === "POST") { try { const body: any = await req.json(); const { worldId, user, password } = body; if (!worldId) { return new Response(JSON.stringify({ error: "Missing worldId" }), { status: 400, headers }); } console.log(`Selecting world targeting: ${worldId}`); currentWorld = worldId; if (user) currentUser = user; if (password !== undefined) currentPassword = password; connectToFoundry(); return new Response(JSON.stringify({ success: true, world: currentWorld, user: currentUser }), { headers }); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } } if (url.pathname === "/api/packages/update" && req.method === "POST") { try { const body: any = await req.json(); const { manifest, type } = body; if (!manifest) { return new Response(JSON.stringify({ error: "Missing manifest URL" }), { status: 400, headers }); } const activeWorld = await getActiveWorldFromJoin(); if (activeWorld) { return new Response(JSON.stringify({ error: `A world (${activeWorld.title}) is currently active. Please shut down the world before updating packages.` }), { status: 400, headers }); } console.log(`Installing/updating package of type ${type || "system"} from manifest: ${manifest}`); const cookie = await getAdminSessionCookie(); const res = await fetch(getFoundryUrl("/setup"), { method: "POST", headers: { "Content-Type": "application/json", Cookie: cookie, }, body: JSON.stringify({ action: "installPackage", type: type || "system", manifest: manifest }), }); if (!res.ok) { const errMsg = await res.text(); throw new Error(`Foundry Setup returned status ${res.status}: ${errMsg}`); } const resData = await res.json(); console.log("Package update response:", resData); return new Response(JSON.stringify({ success: true, details: resData }), { headers }); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); } } if (url.pathname === "/api/worlds/active" && req.method === "GET") { return new Response(JSON.stringify({ world: currentWorld, worldTitle: currentWorldTitle || currentWorld, system: currentSystem, user: currentUser, connected: socket?.connected || false }), { headers }); } return new Response(JSON.stringify({ error: "Not Found" }), { status: 404, headers }); } }); console.log(`Proxy Backend listening on port ${PORT}`); connectToFoundry(); startWatchingCobolUpdates();