3 * @module routes/agentUpdate
4 * @description Agent update and distribution endpoints for binary management and plugin delivery.
5 * Handles agent binary downloads, hash verification, cleanup of old versions, and plugin distribution.
7 * @requires middleware/auth
11 * @author RMM-PSA Development Team
12 * @copyright 2026 RMM-PSA Platform
13 * @license Proprietary
17 * @apiDefine AgentUpdate Agent Updates
18 * Agent binary distribution and version management
21const express = require("express");
22const authenticateToken = require('../middleware/auth');
23const router = express.Router();
24const path = require("path");
25const fs = require("fs");
29 * @api {post} /api/agent/cleanup-binaries Cleanup old binaries
30 * @apiName CleanupAgentBinaries
31 * @apiGroup AgentUpdate
32 * @apiDescription Remove old agent binary versions, retain only latest N versions (default: 3).
33 * Targets files matching pattern: EverydayTechAgent-win-x64-*.exe. Returns deleted and kept files.
34 * @apiParam {number} [keepCount=3] Number of latest versions to retain
35 * @apiSuccess {string[]} deleted Array of deleted filenames
36 * @apiSuccess {string[]} kept Array of retained filenames
37 * @apiExample {curl} Example:
38 * curl -X POST http://localhost:3000/api/agent/cleanup-binaries \\
39 * -H "Authorization: Bearer YOUR_TOKEN" \\
40 * -H "Content-Type: application/json" \\
41 * -d '{"keepCount": 5}'
42 * @apiSuccessExample {json} Success-Response:
46 * "EverydayTechAgent-win-x64-2026.02.15.1200.exe",
47 * "EverydayTechAgent-win-x64-2026.02.10.0900.exe"
50 * "EverydayTechAgent-win-x64-2026.03.12.1400.exe",
51 * "EverydayTechAgent-win-x64-2026.03.10.1000.exe",
52 * "EverydayTechAgent-win-x64-2026.03.01.0800.exe"
56router.post('/cleanup-binaries', authenticateToken, (req, res) => {
57 const base = path.resolve('/opt/apps/IBG_HUB_dist/agent');
58 const keepCount = parseInt(req.body.keepCount, 10) || 3;
59 const files = fs.readdirSync(base).filter(f => f.startsWith('EverydayTechAgent-win-x64-') && f.endsWith('.exe'));
60 // Sort by version/date in filename (assuming format EverydayTechAgent-win-x64-YYYY.MM.DD.HHMM.exe)
61 files.sort((a, b) => b.localeCompare(a)); // descending
62 const toDelete = files.slice(keepCount);
64 for (const file of toDelete) {
66 fs.unlinkSync(path.join(base, file));
69 console.error(`[Cleanup] Failed to delete ${file}:`, err.message);
72 res.json({ deleted, kept: files.slice(0, keepCount) });
76 * @api {get} /api/agent/hash/:version Get agent binary hash
77 * @apiName GetAgentHash
78 * @apiGroup AgentUpdate
79 * @apiDescription Retrieve SHA-256 hash for specified agent version. Checks manifest first,
80 * then calculates hash from binary file. Returns plain text hash string.
81 * Used by agents to verify downloaded binaries for integrity and security.
82 * @apiParam {string} version Agent version (e.g., "1.0.12" or "2026.03.12.1400")
83 * @apiSuccess {string} hash SHA-256 hash (64 hex characters, plain text response)
84 * @apiError (404) {String} error="Agent hash not found" Version not found in manifest or filesystem
85 * @apiError (500) {String} error="Failed to read manifest" Manifest parsing error
86 * @apiExample {curl} Example:
87 * curl -X GET http://localhost:3000/api/agent/hash/1.0.12 \\
88 * -H "Authorization: Bearer YOUR_TOKEN"
89 * @apiSuccessExample {text} Success-Response:
91 * Content-Type: text/plain
93 * a3f2c1b5d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5
95router.get('/hash/:version', authenticateToken, (req, res) => {
96 const version = req.params.version;
97 // Try to get hash from manifest
98 const manifestPath = path.resolve('/opt/apps/IBG_HUB_dist/agent/plugin-manifest.json');
100 if (fs.existsSync(manifestPath)) {
102 const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
103 if (manifest.agentVersion === version && manifest.agentHash) {
104 hash = manifest.agentHash;
105 } else if (manifest.plugins && Array.isArray(manifest.plugins)) {
106 // Try to find agent binary in plugins list
107 const agentPlugin = manifest.plugins.find(p => p.name && p.name.includes('EverydayTechAgent') && p.hash);
108 if (agentPlugin) hash = agentPlugin.hash;
111 return res.status(500).json({ error: 'Failed to read manifest' });
114 // If not found in manifest, calculate hash from file
116 const base = path.resolve('/opt/apps/IBG_HUB_dist/agent');
117 const winBin = path.join(base, `EverydayTechAgent-win-x64-${version}.exe`);
118 const genericBin = path.join(base, `EverydayTechAgent-${version}.exe`);
119 const fallbackBin = path.join(base, `EverydayTechAgent-win-x64.exe`);
121 if (fs.existsSync(winBin)) file = winBin;
122 else if (fs.existsSync(genericBin)) file = genericBin;
123 else if (fs.existsSync(fallbackBin)) file = fallbackBin;
124 if (file && fs.existsSync(file)) {
125 const crypto = require('crypto');
126 const data = fs.readFileSync(file);
127 hash = crypto.createHash('sha256').update(data).digest('hex');
130 if (!hash) return res.status(404).json({ error: 'Agent hash not found' });
131 return res.send(hash);
135 * @api {get} /api/agent/download/:version Download agent binary
136 * @apiName DownloadAgentBinary
137 * @apiGroup AgentUpdate
138 * @apiDescription Download agent binary for specified version. Tries multiple filename patterns:
139 * EverydayTechAgent-win-x64-{version}.exe, EverydayTechAgent-{version}.exe, fallback to latest.
140 * After download, publishes Redis cleanup job to remove old binaries.
141 * @apiParam {string} version Agent version (e.g., "1.0.12" or "2026.03.12.1400")
142 * @apiSuccess {File} file Binary executable file download
143 * @apiError (404) {String} error="Agent binary not found" No matching binary file found
144 * @apiExample {curl} Example:
145 * curl -X GET http://localhost:3000/api/agent/download/1.0.12 \\
146 * -H "Authorization: Bearer YOUR_TOKEN" \\
147 * -o EverydayTechAgent.exe
148 * @apiSuccessExample {binary} Success-Response:
150 * Content-Type: application/octet-stream
151 * Content-Disposition: attachment; filename="EverydayTechAgent-win-x64-1.0.12.exe"
153 * [binary executable content]
155router.get('/download/:version', authenticateToken, (req, res) => {
156 const version = req.params.version;
157 // Map version to expected binary filename
158 // Example: EverydayTechAgent-win-x64.exe for windows
159 // You may want to adjust this logic if you support multiple OSes
160 const base = path.resolve('/opt/apps/IBG_HUB_dist/agent');
161 // Try windows binary first (customize as needed)
162 const winBin = path.join(base, `EverydayTechAgent-win-x64-${version}.exe`);
163 const genericBin = path.join(base, `EverydayTechAgent-${version}.exe`);
164 const fallbackBin = path.join(base, `EverydayTechAgent-win-x64.exe`); // fallback to latest
166 if (fs.existsSync(winBin)) file = winBin;
167 else if (fs.existsSync(genericBin)) file = genericBin;
168 else if (fs.existsSync(fallbackBin)) file = fallbackBin;
169 if (!file) return res.status(404).send('Agent binary not found');
170 return res.download(file);
171 // After serving a new binary, publish a Redis job to clean up old binaries
173 const Redis = require('ioredis');
174 const redis = new Redis(require('../config/redis'));
175 redis.publish('agent-binary-cleanup', JSON.stringify({ action: 'cleanup', keepCount: 3 }));
177 console.log('[AgentUpdate] Published agent-binary-cleanup job to Redis');
179 console.error('[AgentUpdate] Failed to publish cleanup job:', err.message);
184 * Resolve agent binary path by operating system
188function resolveAgentBinary(os) {
189 const base = path.resolve("/opt/apps/IBG_HUB_dist/agent");
192 windows: "EverydayTechAgent-win-x64.exe",
193 linux: "EverydayTechAgent-linux-x64",
194 mac: "EverydayTechAgent-macos-x64"
197 const file = path.join(base, map[os]);
198 return fs.existsSync(file) ? file : null;
202 * @api {get} /api/agent/update/download Download agent by OS
203 * @apiName DownloadAgentByOS
204 * @apiGroup AgentUpdate
205 * @apiDescription Download latest agent binary for specified operating system.
206 * Supports windows, linux, mac. Returns platform-specific binary.
207 * @apiParam {string} os Operating system ("windows", "linux", "mac") - query parameter
208 * @apiSuccess {File} file Binary executable file download for specified OS
209 * @apiError (400) {String} error="missing_os" OS parameter not provided
210 * @apiError (404) {String} error="not_found" No binary available for specified OS
211 * @apiExample {curl} Example (Windows):
212 * curl -X GET "http://localhost:3000/api/agent/update/download?os=windows" \\
213 * -H "Authorization: Bearer YOUR_TOKEN" \\
214 * -o EverydayTechAgent.exe
215 * @apiSuccessExample {binary} Success-Response:
217 * Content-Type: application/octet-stream
218 * Content-Disposition: attachment; filename="EverydayTechAgent-win-x64.exe"
220 * [binary executable content]
222router.get("/update/download", authenticateToken, (req, res) => {
223 const os = req.query.os;
224 if (!os) return res.status(400).json({ error: "missing_os" });
226 const file = resolveAgentBinary(os);
227 if (!file) return res.status(404).json({ error: "not_found" });
229 return res.download(file);
233 * @api {get} /api/agent/:agent_uuid/plugins List agent plugins
234 * @apiName ListAgentPlugins
235 * @apiGroup AgentUpdate
236 * @apiDescription Retrieve available plugins for agent. Returns base64-encoded plugin binaries
237 * from plugins directory (.exe, .sh, .pkg files). Used by agents to sync plugin catalog.
238 * @apiParam {string} agent_uuid Agent UUID (path parameter, currently unused for filtering)
239 * @apiSuccess {object[]} plugins Array of plugin objects
240 * @apiSuccess {string} plugins.name Plugin filename (e.g., "rustdesk-installer.exe")
241 * @apiSuccess {string} plugins.binary Base64-encoded plugin binary data
242 * @apiError (404) {String} error="Plugins directory not found" Plugin directory missing
243 * @apiExample {curl} Example:
244 * curl -X GET http://localhost:3000/api/agent/abc-123-uuid/plugins \\
245 * -H "Authorization: Bearer YOUR_TOKEN"
246 * @apiSuccessExample {json} Success-Response:
250 * "name": "rustdesk-installer.exe",
251 * "binary": "TVqQAAMAAAAEAAAA//8AALgAAAAA..."
254 * "name": "network-scanner.sh",
255 * "binary": "IyEvYmluL2Jhc2gKZWNobyAiU2Nhbm5pbmcu..."
259router.get('/:agent_uuid/plugins', authenticateToken, (req, res) => {
260 const agent_uuid = req.params.agent_uuid;
261 // For now, serve all plugins in the plugins directory
262 // Optionally, filter by agent_uuid if needed
263 const pluginsDir = path.resolve('/opt/apps/IBG_HUB_dist/agent/plugins');
264 if (!fs.existsSync(pluginsDir)) return res.status(404).json({ error: 'Plugins directory not found' });
265 const files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.exe') || f.endsWith('.sh') || f.endsWith('.pkg'));
266 const plugins = files.map(name => {
267 const filePath = path.join(pluginsDir, name);
268 const binary = fs.readFileSync(filePath).toString('base64');
269 return { name, binary };
274module.exports = router;