import http from "node:http"; import { join } from "node:path"; import { readFileSync, readdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; import vm from "node:vm"; import axios from "axios"; import { v4 as uuidv4 } from "uuid"; import jwt from "jsonwebtoken"; import { create as xmlBuilder } from "xmlbuilder2"; import { logger } from "./logger.js"; import { createClient } from "redis"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const globalVMContext = { URLSearchParams, URL, console: logger, crypto, axios, uuidv4, jwt, xmlBuilder, }; let globalVariableContext = {}; /** * Load all files from globalVariables/ directory into globalVariableContext * Pattern: globalVariables/filename.{json|js} -> globalVariableContext['filename'] */ function loadGlobalVariables() { const globalDir = join(__dirname, "globalVariables"); const jsonFiles = []; const jsFiles = []; // Scan and categorize files in one pass readdirSync(globalDir).forEach((file) => { if (file.includes(".example")) return; if (file.endsWith(".json")) jsonFiles.push(file); else if (file.endsWith(".js")) jsFiles.push(file); }); // Load JSON files first (data) jsonFiles.forEach((file) => { const varName = file.replace(".json", ""); const data = JSON.parse(readFileSync(join(globalDir, file), "utf-8")); globalVariableContext[varName] = data; logger.info({ message: `Loaded global data: ${varName}`, keys: Object.keys(data) }); }); // Load JS files second (functions can reference JSON data) jsFiles.forEach((file) => { const varName = file.replace(".js", ""); const code = readFileSync(join(globalDir, file), "utf-8"); // Wrap the literal function body in a function and execute const wrappedCode = `(function() {\n${code}\n})()`; const script = new vm.Script(wrappedCode, { filename: file }); const context = vm.createContext({ ...globalVMContext, ...globalVariableContext }); // Execute script and capture returned object const returnedObject = script.runInContext(context); globalVariableContext[varName] = returnedObject; logger.info({ message: `Loaded global functions: ${varName}`, type: typeof returnedObject, isObject: typeof returnedObject === 'object' && returnedObject !== null, keys: returnedObject ? Object.keys(returnedObject).length : 0 }); }); logger.info({ message: `Loaded ${jsonFiles.length + jsFiles.length} global variables`, json: jsonFiles.length, js: jsFiles.length }); } /** * Load configuration from config/default.json and merge with environment variables */ function loadConfig() { const configPath = join(__dirname, "..", "config", "default.json"); const configData = readFileSync(configPath, "utf-8"); const config = JSON.parse(configData); // Merge environment variables (ENV takes precedence) return { ...config, server: { ...config.server, port: process.env.PORT ? parseInt(process.env.PORT, 10) : config.server.port, host: process.env.HOST || config.server.host, }, proxy: { ...config.proxy, }, logging: { ...config.logging, level: process.env.LOG_LEVEL || config.logging.level, }, }; } /** * Validate configuration */ function validateConfig(config) { if (!config.server.port || config.server.port < 1 || config.server.port > 65535) { throw new Error("Invalid server.port (must be 1-65535)"); } } /** * Start the HTTP server */ async function startServer() { try { // Connect Redis client before loading global variables const redisClient = createClient(); await redisClient.connect(); globalVMContext.redis = redisClient; // Load configuration into global.config global.config = loadConfig(); // Load all global variables (JSON data + JS function modules) loadGlobalVariables(); logger.info("Starting Proxy Script Server..."); logger.info({ message: "Configuration loaded", port: global.config.server.port, host: global.config.server.host, logLevel: global.config.logging.level, proxyPrefix: global.config.proxy ? `${global.config.proxy.pathPrefix}${global.config.proxy.workspaceId}/${global.config.proxy.branch}/${global.config.proxy.routeName}` : '(none)' }); // Validate configuration validateConfig(global.config); logger.info("Configuration validated successfully"); const proxyPath = join(__dirname, "proxyScripts", "kmeContentSourceAdapter.js"); const proxyCode = readFileSync(proxyPath, "utf-8"); const script = new vm.Script(proxyCode, { filename: "proxy.js" }); // Create HTTP server that delegates all requests to proxy const server = http.createServer((req, res) => { try { // Extract proxy routing metadata from config (not parsed from URL) // and attach to req.params if URL matches the configured prefix if (global.config.proxy) { const { pathPrefix, workspaceId, branch, routeName } = global.config.proxy; const fullPrefix = `${pathPrefix.replace(/\/$/, '')}/${workspaceId}/${branch}/${routeName}`; // Check if URL starts with proxy prefix if (req.url.startsWith(fullPrefix)) { // Add routing metadata to request for proxy.js // All values come from config, not parsed from URL req.params = { "0": req.url, // Original full path workspaceId, // From config.proxy.workspaceId branch, // From config.proxy.branch route: routeName // From config.proxy.routeName (renamed to 'route' for consistency) }; } } const context = vm.createContext({ ...globalVMContext, ...globalVariableContext, req, res, }); script.runInContext(context); } catch (error) { logger.error({ message: "Request handling failed", error: error.message, stack: error.stack }); res.statusCode = 500; res.end("Internal Server Error"); } }); // Graceful shutdown const shutdown = () => { logger.info("\nShutting down gracefully..."); server.close(() => { logger.info("Server closed"); process.exit(0); }); // Force shutdown after 10 seconds setTimeout(() => { logger.error("Forced shutdown after timeout"); process.exit(1); }, 10000); }; process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); // Start listening server.listen(global.config.server.port, global.config.server.host, () => { logger.info({ message: "Server listening", port: global.config.server.port, host: global.config.server.host }); }); } catch (error) { logger.error({ message: "Failed to start server", error: error.message, stack: error.stack }); process.exit(1); } } // Start the server startServer();