netanalyzer/remoteAgent.js
Joseph McQueen 0a46d25bd6 chore: project cleanup — dead code, logging, tests, WS hardening
- Delete unused dataMerger, formatter, Device model, dead service exports,
  and the empty agents/ + .gitkeep placeholders.
- Extract STORE_MODES and MDM device-type filters into a shared constants.js.
- Anchor bot regexes (^help|^store|^analyze) so "analyze store 305" no
  longer fires both handlers; replace catch-all noise.
- Hoist inline require() calls in integrations to top-of-file imports.
- Harden WebSocket server: Authorization header support, single-agent
  enforcement, bounded pending requests, server-level error handler,
  coalesced cache refresh in Meraki client.
- Wrap Meraki/MDM network calls with withRetry; add request timeouts.
- Migrate all console.* calls onto utils/logger.js (LOG_LEVEL aware);
  drive Webex framework logLevel from env.
- Refactor storeDetail.js: shared renderClientLine + buildMdmSection
  helpers cut duplication roughly in half.
- Refresh README structure, document LOG_LEVEL, add npm run agent script,
  add jest testMatch + new tests (handlers, HealthReport, Store, ws).

Verified: npm run lint clean, 7 suites / 31 tests passing.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 17:21:22 -04:00

120 lines
2.9 KiB
JavaScript

const WebSocket = require('ws');
const axios = require('axios');
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;
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 connected to NetAnalyzer');
reconnectAttempts = 0;
});
ws.on('message', async data => {
let request;
try {
request = JSON.parse(data);
if (request.action !== 'proxyRequest') return;
console.log(`🔄 Proxying ${request.method || 'GET'} ${request.url}`);
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,
});
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 (_e) {
// ignore close errors during shutdown
}
}
process.exit(0);
}
process.on('SIGINT', () => shutdownRemote('SIGINT'));
process.on('SIGTERM', () => shutdownRemote('SIGTERM'));