const WebSocket = require('ws'); const axios = require('axios'); const https = require('https'); const { version: AGENT_VERSION } = require('./package.json'); require('dotenv').config(); const WS_URL = process.env.WS_URL; const WS_TOKEN = process.env.WS_TOKEN; if (!WS_URL) { console.error('WS_URL is not set in .env'); process.exit(1); } const INITIAL_BACKOFF_MS = 2000; const MAX_BACKOFF_MS = 60000; const PROXY_TIMEOUT_MS = 30000; // Capabilities advertised in the hello handshake so the bot can log what // this agent understands. Keep additive; older bots ignore unknown caps. const AGENT_CAPABILITIES = ['insecure', 'hello']; // Shared https.Agent used only when the bot flags a proxied request with // `insecure: true` (e.g. reaching Store Info Web, which is served with an // internal-CA cert Node doesn't know about). All other requests use axios's // default (validated) TLS. Kept as a module-level singleton so we don't // leak sockets per request. const insecureHttpsAgent = new https.Agent({ rejectUnauthorized: false }); let ws = null; let reconnectAttempts = 0; let shuttingDown = false; /** * If WS_TOKEN is provided, send it as an Authorization: Bearer header so the * secret stays out of access logs. (The server still accepts the legacy * ?token=... query parameter for backward compatibility.) */ function buildClientOptions() { if (!WS_TOKEN) return undefined; return { headers: { Authorization: `Bearer ${WS_TOKEN}` } }; } function connect() { console.log(`Connecting to ${WS_URL}...`); ws = new WebSocket(WS_URL, buildClientOptions()); ws.on('open', () => { console.log(`Remote Agent v${AGENT_VERSION} connected to wbxStoreProvision`); reconnectAttempts = 0; // One-shot hello so the bot can log which agent version + capabilities // are on the other end of the socket. Fire-and-forget — the bot // tolerates its absence for backward compatibility with v1.1 agents. try { ws.send( JSON.stringify({ type: 'hello', version: AGENT_VERSION, capabilities: AGENT_CAPABILITIES, }), ); } catch (err) { console.error('Failed to send hello:', err.message); } }); ws.on('message', async (data) => { let request; try { request = JSON.parse(data); if (request.action !== 'proxyRequest') return; const insecure = request.insecure === true; console.log( `Proxying ${request.method || 'GET'} ${request.url}${insecure ? ' (insecure TLS)' : ''}`, ); const response = await axios({ method: request.method || 'GET', url: request.url, headers: request.headers || {}, auth: request.auth || undefined, data: request.body || undefined, timeout: PROXY_TIMEOUT_MS, ...(insecure ? { httpsAgent: insecureHttpsAgent } : {}), }); ws.send( JSON.stringify({ requestId: request.requestId, status: response.status, data: response.data, headers: response.headers, }), ); } catch (err) { console.error('Proxy error:', err.message); ws.send( JSON.stringify({ requestId: request ? request.requestId : null, error: err.message, status: err.response?.status || 500, data: err.response?.data || null, }), ); } }); ws.on('close', (code) => { console.log(`Disconnected (code: ${code}).`); if (!shuttingDown) scheduleReconnect(); }); ws.on('error', (err) => { console.error('WebSocket error:', err.message); }); } function scheduleReconnect() { reconnectAttempts++; const backoff = Math.min( INITIAL_BACKOFF_MS * Math.pow(1.5, reconnectAttempts - 1), MAX_BACKOFF_MS, ); console.log( `Reconnecting in ${Math.round(backoff / 1000)}s... (attempt ${reconnectAttempts})`, ); setTimeout(connect, backoff); } connect(); function shutdownRemote(signal) { console.log(`${signal} received. Shutting down remote agent...`); shuttingDown = true; if (ws) { try { ws.close(); } catch { // ignore close errors during shutdown } } process.exit(0); } process.on('SIGINT', () => shutdownRemote('SIGINT')); process.on('SIGTERM', () => shutdownRemote('SIGTERM'));