- Agent hello handshake: agent (v1.2.0) sends {type, version, capabilities}
on connect; bot logs "Agent hello: v1.2.0 (capabilities: insecure, hello)"
and exposes getAgentInfo(). Backward-compatible with older agents.
- proxyRequest tracks method + url per request; all error paths (agent
errors, timeouts, disconnect rejects, send failures) now include
"(for METHOD URL)" so the failing endpoint is unambiguous
- Add requireAgent(bot) preflight; buildStore/stageStore/migrateStore
reject up front when the agent is disconnected instead of failing
mid-flow after partial Webex mutations
- Add parseStoreNumber (^\d{1,5}$) and wire into store-number commands
with proper usage messages; add loose email-shape check to /userInfo
- Fix pre-existing catch(_e) lint warning in remoteAgent.js (bare catch)
Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
4.7 KiB
JavaScript
148 lines
4.7 KiB
JavaScript
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'));
|