3 * @description Agent installer and plugin download distribution system. Handles bootstrapper generation (dynamic/static),
4 * dev-agent installers with JWT token injection, plugin binary serving, enrollment token generation,
5 * and checksum verification. Supports Windows/Linux/Mac with Inno Setup and Wine-based builds.
6 * @module routes/download
10 * @apiDefine DownloadGroup Agent Download & Distribution
11 * Agent installer, bootstrapper, and plugin distribution endpoints
14const express = require('express');
15const path = require('path');
16const fs = require('fs');
17const authenticateToken = require('../middleware/auth');
18const router = express.Router();
19const { exec, execFileSync } = require('child_process');
20const os = require('os');
23 * @api {get} /api/download/bootstrapper-dynamic/:tenantId.exe Dynamic Bootstrapper EXE Generator
24 * @apiName GetDynamicBootstrapper
25 * @apiGroup DownloadGroup
26 * @apiDescription Dynamically generates tenant-specific bootstrapper EXE with embedded JWT token. Builds via Node.js
27 * builder script (build_bootstrapper.js) and caches result for future downloads. No authentication
28 * required. Build timeout: 5 minutes.
29 * @apiParam {string} tenantId Tenant ID for JWT embedding
30 * @apiSuccess (200) {Binary} file Bootstrapper EXE binary
31 * @apiHeader {string} Content-Disposition attachment; filename=bootstrapper_${tenantId}.exe
32 * @apiError 400 Missing tenantId
33 * @apiError 500 Missing AGENT_SIGN_KEY or build/download failed
34 * @apiExample {curl} Example:
35 * curl -O -J https://api.example.com/api/download/bootstrapper-dynamic/tenant-123.exe
37// Dynamic bootstrapper EXE generator
38const jwt = require('jsonwebtoken');
39const AGENT_SIGN_KEY = process.env.AGENT_SIGN_KEY;
40router.get('/bootstrapper-dynamic/:tenantId.exe', async (req, res) => {
41 const tenantId = req.params.tenantId;
42 console.log(`[BOOTSTRAPPER] Incoming request for tenantId: ${tenantId}`);
44 // Extend timeout for build process (5 minutes)
45 req.setTimeout(5 * 60 * 1000);
46 res.setTimeout(5 * 60 * 1000);
48 // Authenticate user and determine tenant context here if needed
50 console.error('[BOOTSTRAPPER] Missing tenantId');
51 return res.status(400).json({ error: 'Missing tenantId' });
53 if (!AGENT_SIGN_KEY) {
54 console.error('[BOOTSTRAPPER] Missing AGENT_SIGN_KEY');
55 return res.status(500).json({ error: 'Missing AGENT_SIGN_KEY' });
57 // Generate JWT for agent onboarding
58 const token = jwt.sign({ tenantId }, AGENT_SIGN_KEY, { expiresIn: '30d' });
59 console.log(`[BOOTSTRAPPER] Generated JWT for tenantId: ${tenantId}`);
61 // Use Node.js builder script
62 const builderScript = path.resolve(__dirname, '../downloads/build_bootstrapper.js');
63 const downloadsDir = path.resolve(__dirname, '../downloads');
64 const outputExe = path.join(downloadsDir, `bootstrapper_${tenantId}.exe`);
65 // Defensive: ensure outputExe is always in backend/downloads
66 if (!outputExe.startsWith(downloadsDir)) {
67 console.error(`[BOOTSTRAPPER] Output path is not in backend/downloads: ${outputExe}`);
68 return res.status(500).json({ error: 'Output path misconfiguration', details: outputExe });
70 // If EXE already exists, serve it
71 if (fs.existsSync(outputExe)) {
72 console.log(`[BOOTSTRAPPER] Found existing EXE for tenantId: ${tenantId}, serving.`);
73 return res.download(outputExe, `bootstrapper_${tenantId}.exe`);
76 console.log(`[BOOTSTRAPPER] Building EXE for tenantId: ${tenantId} using builder script: ${builderScript}`);
77 // Synchronously build the EXE and keep it for future downloads
78 execFileSync('node', [builderScript, tenantId, outputExe], {
80 timeout: 4 * 60 * 1000, // 4 minute timeout for build
81 maxBuffer: 10 * 1024 * 1024 // 10MB buffer
83 console.log(`[BOOTSTRAPPER] Build complete, streaming EXE: ${outputExe}`);
84 res.download(outputExe, `bootstrapper_${tenantId}.exe`, (err) => {
86 console.error(`[BOOTSTRAPPER] Download error: ${err.message}`);
88 console.log(`[BOOTSTRAPPER] Download successful for tenantId: ${tenantId}`);
90 // Do NOT delete outputExe; keep for future downloads
93 console.error(`[BOOTSTRAPPER] Build or download failed for tenantId: ${tenantId}: ${err.message}`);
94 return res.status(500).json({ error: 'Build or download failed', details: err.message });
99 * @api {get} /api/download/bootstrapper-static/:tenantId.exe Static Bootstrapper EXE
100 * @apiName GetStaticBootstrapper
101 * @apiGroup DownloadGroup
102 * @apiDescription Serves pre-built tenant-specific static bootstrapper EXE from downloads folder.
103 * No dynamic generation. No authentication required.
104 * @apiParam {string} tenantId Tenant ID
105 * @apiSuccess (200) {Binary} file Bootstrapper EXE binary
106 * @apiHeader {string} Content-Disposition attachment; filename=bootstrapper_${tenantId}.exe
107 * @apiError 404 Bootstrapper not found
108 * @apiExample {curl} Example:
109 * curl -O -J https://api.example.com/api/download/bootstrapper-static/tenant-123.exe
111// Serve tenant-specific static bootstrapper EXE
112router.get('/bootstrapper-static/:tenantId.exe', async (req, res) => {
113 const filePath = path.resolve(__dirname, `../downloads/bootstrapper_${req.params.tenantId}.exe`);
114 if (!fs.existsSync(filePath)) {
115 return res.status(404).json({ error: 'Bootstrapper not found' });
117 res.download(filePath, `bootstrapper_${req.params.tenantId}.exe`);
122 * @api {get} /api/download/bootstrapper.exe Default Bootstrapper EXE
123 * @apiName GetDefaultBootstrapper
124 * @apiGroup DownloadGroup
125 * @apiDescription Serves default static bootstrapper EXE (no tenant specificity) for Syncro-style installer.
126 * No authentication required.
127 * @apiSuccess (200) {Binary} file Bootstrapper EXE binary
128 * @apiHeader {string} Content-Disposition attachment; filename=bootstrapper.exe
129 * @apiError 404 Bootstrapper not found
130 * @apiExample {curl} Example:
131 * curl -O -J https://api.example.com/api/download/bootstrapper.exe
133// Serve static bootstrapper EXE for Syncro-style installer (no auth)
134router.get('/bootstrapper.exe', async (req, res) => {
135 const bootstrapperPath = path.resolve(__dirname, '../downloads/bootstrapper.exe');
136 if (!fs.existsSync(bootstrapperPath)) {
137 return res.status(404).json({ error: 'Bootstrapper not found' });
139 res.download(bootstrapperPath, 'bootstrapper.exe');
143 * @api {get} /api/download/dev-agent/quick/:tenantId Quick Install Batch Script
144 * @apiName GetQuickInstallScript
145 * @apiGroup DownloadGroup
146 * @apiDescription Generates tenant-specific quick install batch script (.bat) for Windows. Creates bootstrap.json
147 * in ProgramData with embedded JWT (365-day expiration), downloads Inno Setup installer, and runs
148 * silent installation. No authentication required.
149 * @apiParam {string} tenantId Tenant ID for JWT embedding
150 * @apiSuccess (200) {string} file Batch script (.bat)
151 * @apiHeader {string} Content-Type application/octet-stream
152 * @apiHeader {string} Content-Disposition attachment; filename=Install_Agent.bat
153 * @apiError 400 Missing tenantId
154 * @apiError 500 Missing AGENT_SIGN_KEY or generation failed
155 * @apiExample {curl} Example:
156 * curl -O -J https://api.example.com/api/download/dev-agent/quick/tenant-123
157 * @apiExample {txt} Batch Script Output:
159 * REM EverydayTech Agent Quick Install
160 * echo Creating bootstrap configuration...
161 * echo { > "%PROGRAMDATA%\EverydayTechAgent\bootstrap.json"
164// Quick install - downloads PowerShell GUI installer as EXE (built via Wine)
165router.get('/dev-agent/quick/:tenantId', async (req, res) => {
166 const tenantId = req.params.tenantId;
169 return res.status(400).json({ error: 'Missing tenantId' });
172 const AGENT_SIGN_KEY = process.env.AGENT_SIGN_KEY;
173 if (!AGENT_SIGN_KEY) {
174 return res.status(500).json({ error: 'Missing AGENT_SIGN_KEY' });
177 const jwtToken = jwt.sign({ tenantId }, AGENT_SIGN_KEY, { expiresIn: '365d' });
180 console.log(`[QUICK-INSTALL] Generating batch installer for tenant: ${tenantId}`);
182 // Create batch script that downloads Inno Setup installer
183 const batchScript = `@echo off
184REM EverydayTech Agent Quick Install
185REM Generated for tenant: ${tenantId}
188echo ========================================
189echo EverydayTech Agent Installer
190echo ========================================
193REM Create bootstrap.json in agent data directory
194echo Creating bootstrap configuration...
195set "DATADIR=%PROGRAMDATA%\\EverydayTechAgent"
196if not exist "%DATADIR%" mkdir "%DATADIR%"
198echo { > "%DATADIR%\\bootstrap.json"
199echo "tenantId": "${tenantId}", >> "%DATADIR%\\bootstrap.json"
200echo "jwt": "${jwtToken}", >> "%DATADIR%\\bootstrap.json"
201echo "backend": "https://everydaytech.au/api" >> "%DATADIR%\\bootstrap.json"
202echo } >> "%DATADIR%\\bootstrap.json"
204echo Bootstrap configuration created.
207echo Downloading installer...
208set "INSTALLER=%TEMP%\\EverydayTechAgent_Installer.exe"
210powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri 'https://everydaytech.au/api/download/dev-agent' -OutFile '%INSTALLER%' -UseBasicParsing"
212if not exist "%INSTALLER%" (
214 echo ERROR: Download failed. Please check your internet connection.
221echo Starting installation...
224REM Run Inno Setup installer (will detect bootstrap.json)
225"%INSTALLER%" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
228echo Installation complete! The agent will appear in your dashboard shortly.
234 // Send as downloadable batch file
235 res.setHeader('Content-Type', 'application/octet-stream');
236 res.setHeader('Content-Disposition', 'attachment; filename="Install_Agent.bat"');
237 res.send(batchScript);
240 console.error(`[QUICK-INSTALL] Failed to generate installer: ${err.message}`);
241 return res.status(500).json({ error: 'Failed to generate installer', details: err.message });
246 * @api {get} /api/download/dev-agent/bootstrap.ps1 PowerShell Bootstrap Script (Legacy)
247 * @apiName GetBootstrapScript
248 * @apiGroup DownloadGroup
249 * @apiDescription Generates legacy PowerShell bootstrap script for agent installation. Requires tenant and token
250 * query parameters. Downloads installer and runs with tenant/token arguments. No authentication required.
251 * @apiParam (Query) {string} tenant Tenant ID
252 * @apiParam (Query) {string} token JWT token for agent enrollment
253 * @apiSuccess (200) {string} file PowerShell script (.ps1)
254 * @apiHeader {string} Content-Type application/x-powershell
255 * @apiHeader {string} Content-Disposition attachment; filename=agent_bootstrap_${tenant}.ps1
256 * @apiError 400 Missing tenant or token query parameter
257 * @apiExample {curl} Example:
258 * curl -O -J "https://api.example.com/api/download/dev-agent/bootstrap.ps1?tenant=tenant-123&token=eyJhbGc..."
260// Legacy PowerShell bootstrap script generator (keeping for compatibility)
261router.get('/dev-agent/bootstrap.ps1', async (req, res) => {
262 const { tenant, token } = req.query;
264 return res.status(400).json({ error: 'Missing ?tenant=' });
266 return res.status(400).json({ error: 'Missing ?token=' });
268 const installerUrl = `https://everydaytech.au/api/download/dev-agent?tenant=${encodeURIComponent(tenant)}&token=${encodeURIComponent(token)}`;
272$installer = "$env:TEMP\\EverydayTechAgent_Installer.exe"
274Invoke-WebRequest "${installerUrl}" -OutFile $installer
275Start-Process $installer -ArgumentList "/tenant=$tenant /token=$token" -Wait
277 res.setHeader('Content-Type', 'application/x-powershell');
278 res.setHeader('Content-Disposition', `attachment; filename=agent_bootstrap_${tenant}.ps1`);
283 * @api {get} /api/download/dev-agent Dev Agent Installer
284 * @apiName GetDevAgentInstaller
285 * @apiGroup DownloadGroup
286 * @apiDescription Downloads pre-built dev-agent Inno Setup installer (.exe) with optional tenant context.
287 * If tenant query parameter provided, generates JWT and customizes filename. Sets no-cache headers
288 * to prevent Cloudflare caching. No authentication required.
289 * @apiParam (Query) {string} [tenant] Optional tenant ID for JWT generation and custom filename
290 * @apiParam (Query) {string} [token] Optional legacy token parameter (unused)
291 * @apiSuccess (200) {Binary} file Inno Setup installer EXE
292 * @apiHeader {string} Content-Disposition attachment; filename=EverydayTechAgent_${tenantId}.exe or EverydayTechAgent_Installer.exe
293 * @apiHeader {string} Cache-Control no-store, no-cache, must-revalidate
294 * @apiError 404 Installer not built (run: builder/dev/installer/build_installer.sh)
295 * @apiExample {curl} Example with tenant:
296 * curl -O -J "https://api.example.com/api/download/dev-agent?tenant=tenant-123"
297 * @apiExample {curl} Example without tenant:
298 * curl -O -J https://api.example.com/api/download/dev-agent
300router.get('/dev-agent', async (req, res) => {
301 const { tenant, token } = req.query;
303 // Path to pre-built installer
304 const installerPath = path.resolve(__dirname, '../downloads/dev-agent.exe');
306 if (!fs.existsSync(installerPath)) {
307 return res.status(404).json({ error: 'Installer not built yet. Run: builder/dev/installer/build_installer.sh' });
310 console.log(`[DEV-INSTALLER] Download requested for tenant=${tenant || 'none'}`);
312 // Set cache control headers to prevent Cloudflare caching
313 res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
314 res.setHeader('Pragma', 'no-cache');
315 res.setHeader('Expires', '0');
316 res.setHeader('Surrogate-Control', 'no-store');
318 // If tenant provided, generate JWT and write bootstrap.json hint
320 const AGENT_SIGN_KEY = process.env.AGENT_SIGN_KEY;
321 if (AGENT_SIGN_KEY) {
322 const jwtToken = jwt.sign({ tenantId: tenant }, AGENT_SIGN_KEY, { expiresIn: '30d' });
323 console.log(`[DEV-INSTALLER] Generated JWT for tenant ${tenant}`);
325 // Optional: Set custom filename with tenant ID for tracking
326 const filename = `EverydayTechAgent_${tenant.substring(0, 8)}.exe`;
327 return res.download(installerPath, filename);
331 // Default download without tenant context
332 res.download(installerPath, 'EverydayTechAgent_Installer.exe');
336 * EverydayTechAgent Download / Installer Route (Patched)
337 * -----------------------------------------------------
338 * Adds dynamic JWT + state/config.json injection into installer package
344 * @api {get} /api/download/:pluginName Plugin Binary Download (Catch-All)
345 * @apiName GetPluginBinary
346 * @apiGroup DownloadGroup
347 * @apiDescription Downloads plugin binary from agent/plugins folders. Searches agent/plugins/agent first,
348 * then OS-specific folders (windows, linux, mac). Returns 404 with available plugins list if not found.
349 * Requires authentication.
350 * @apiParam {string} pluginName Plugin filename/path
351 * @apiSuccess (200) {Binary} file Plugin binary
352 * @apiError 404 Plugin not found (includes available plugins list)
353 * @apiExample {curl} Example:
354 * curl https://api.example.com/api/download/network-scanner.exe \
355 * -H "Authorization: Bearer YOUR_TOKEN" \
357 * @apiExample {json} Error-Response (404):
359 * "error": "Plugin not found",
360 * "requested": "network-scanner.exe",
361 * "availablePlugins": ["plugin1.exe", "plugin2.sh"],
362 * "searchedDirs": ["/path/to/agent/plugins/agent/network-scanner.exe", ...]
365// Serve plugin binaries: /api/plugins/:pluginName
366router.get('/:pluginName', authenticateToken, async (req, res) => {
367 const pluginName = req.params.pluginName;
368 console.log(`[Plugin DL] ${pluginName} requested`);
369 // Try all OS plugin folders and agent binary folder
370 const osList = ['windows', 'linux', 'mac'];
371 let foundPath = null;
372 const projectRoot = path.resolve(__dirname, '../../');
373 // Check agent binary folder first
374 const agentBinPath = path.join(projectRoot, 'agent', 'plugins', 'agent', pluginName);
375 console.log(`[PLUGIN DL] Checking agent binary: ${agentBinPath}`);
376 if (fs.existsSync(agentBinPath)) {
377 foundPath = agentBinPath;
379 for (const os of osList) {
380 const pluginPath = path.join(projectRoot, 'agent', 'plugins', os, pluginName);
381 console.log(`[PLUGIN DL] Checking: ${pluginPath}`);
382 if (fs.existsSync(pluginPath)) {
383 foundPath = pluginPath;
389 // Gather available plugins for better error reporting
390 const availablePlugins = [];
392 // Check agent/plugins/agent
393 const agentDir = path.join(projectRoot, 'agent', 'plugins', 'agent');
394 if (fs.existsSync(agentDir)) {
395 availablePlugins.push(...fs.readdirSync(agentDir));
397 // Check OS plugin folders
398 for (const os of osList) {
399 const osDir = path.join(projectRoot, 'agent', 'plugins', os);
400 if (fs.existsSync(osDir)) {
401 availablePlugins.push(...fs.readdirSync(osDir));
405 console.error('[Plugin DL] Error listing plugins:', err);
407 return res.status(404).json({
408 error: 'Plugin not found',
409 requested: pluginName,
411 searchedDirs: [agentBinPath, ...osList.map(os => path.join(projectRoot, 'agent', 'plugins', os, pluginName))]
414 res.sendFile(foundPath);
417module.exports = router;
420 * @api {get} /api/download/plugin-manifest.json Plugin Manifest
421 * @apiName GetPluginManifest
422 * @apiGroup DownloadGroup
423 * @apiDescription Retrieves plugin manifest with name, SHA-256 hash, and download URL from database.
424 * Used by agents to check for plugin updates. Requires authentication.
425 * @apiSuccess {object} manifest Plugin manifest
426 * @apiSuccess {object[]} manifest.plugins Array of plugin records
427 * @apiSuccess {string} manifest.plugins.name Plugin name
428 * @apiSuccess {string} manifest.plugins.hash SHA-256 hash
429 * @apiSuccess {string} manifest.plugins.url Download URL
430 * @apiError 500 Database error
431 * @apiExample {curl} Example:
432 * curl https://api.example.com/api/download/plugin-manifest.json \
433 * -H "Authorization: Bearer YOUR_TOKEN"
434 * @apiExample {json} Success-Response (200):
438 * "name": "network-scanner",
439 * "hash": "abc123...",
440 * "url": "https://api.example.com/api/download/network-scanner.exe"
445// ----------------------
446// Plugin manifest passthrough
447// ----------------------
448const pool = require('../services/db');
449router.get('/plugin-manifest.json', authenticateToken, async (req, res) => {
451 const result = await pool.query('SELECT name, hash, url FROM plugins');
452 res.json({ plugins: result.rows });
454 console.error('[Plugin Manifest] DB error:', err);
455 res.status(500).json({ error: 'DB error' });
460 * @api {get} /api/download/:os/config Config Route for Bare Agent (Legacy)
461 * @apiName GetAgentConfig
462 * @apiGroup DownloadGroup
463 * @apiDescription Legacy endpoint returning minimal JWT token for bare agent configuration. Requires authentication.
464 * @apiParam {string} os Operating system (unused)
465 * @apiSuccess {string} jwt JWT token (30-day expiration)
466 * @apiExample {curl} Example:
467 * curl https://api.example.com/api/download/windows/config \
468 * -H "Authorization: Bearer YOUR_TOKEN"
469 * @apiExample {json} Success-Response (200):
471 * "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
474// ----------------------
475// Config route for bare agent (legacy)
476// ----------------------
477router.get('/:os/config', authenticateToken, (req, res) => {
478 const jwtToken = jwt.sign({}, JWT_SECRET, { expiresIn: '30d' });
479 res.json({ jwt: jwtToken });
483 * @api {post} /api/download/enroll Enrollment Token Generator
484 * @apiName GenerateEnrollmentToken
485 * @apiGroup DownloadGroup
486 * @apiDescription Generates agent enrollment token with new agent UUID, tenant ID, and optional customer ID.
487 * Token valid for 30 days. Requires authentication.
488 * @apiParam {string} tenantId Tenant ID for agent enrollment
489 * @apiParam {string} [customerId] Optional customer ID
490 * @apiSuccess {string} agentId Generated agent UUID (v4)
491 * @apiSuccess {string} token JWT enrollment token (30-day expiration)
492 * @apiError 400 Missing tenantId
493 * @apiError 500 Enrollment failed
494 * @apiExample {curl} Example:
495 * curl -X POST https://api.example.com/api/download/enroll \
496 * -H "Authorization: Bearer YOUR_TOKEN" \
497 * -H "Content-Type: application/json" \
498 * -d '{"tenantId":"tenant-123", "customerId":"cust-456"}'
499 * @apiExample {json} Success-Response (200):
501 * "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
502 * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
505// ----------------------
506// Enrollment token generator
507// ----------------------
508router.post('/enroll', authenticateToken, (req, res) => {
510 const { tenantId, customerId } = req.body;
511 if (!tenantId) return res.status(400).json({ error: 'Missing tenantId' });
512 const agentId = uuidv4();
513 const token = jwt.sign({ agentId, tenantId, customerId }, JWT_SECRET, { expiresIn: '30d' });
514 res.json({ agentId, token });
516 console.error('[Enroll] Failed:', err);
517 res.status(500).json({ error: 'Enrollment failed' });
526function execPromise(cmd) {
527 const { exec } = require('child_process');
528 return new Promise((resolve, reject) =>
529 exec(cmd, (err, stdout, stderr) => (err ? reject(stderr || err) : resolve(stdout)))
535 * @api {get} /api/download/:os/tenant Tenant-Only Direct Download
536 * @apiName GetTenantDirectDownload
537 * @apiGroup DownloadGroup
538 * @apiDescription Downloads OS-specific agent installer or binary for tenant without config injection.
539 * Uses FILE_MAP for OS lookup. Requires authentication + 't' query parameter with JWT token.
540 * @apiParam {string} os Operating system (windows, linux, mac)
541 * @apiParam (Query) {String} t JWT token for verification
542 * @apiParam (Query) {String} [type=installer] File type (installer, agent, etc.)
543 * @apiSuccess (200) {Binary} file Agent installer or binary
544 * @apiError 400 Missing token, unsupported OS, or invalid/expired token
545 * @apiError 404 File not found for OS/type combination
546 * @apiExample {curl} Example:
547 * curl -O -J "https://api.example.com/api/download/windows/tenant?t=eyJhbGc...&type=installer" \
548 * -H "Authorization: Bearer YOUR_TOKEN"
550// ----------------------
551// Tenant-only direct download (no config injection)
552// ----------------------
553router.get('/:os/tenant', authenticateToken, (req, res) => {
555 const token = req.query.t;
556 if (!token) return res.status(400).send('Missing token');
557 const decoded = jwt.verify(token, JWT_SECRET);
558 const osName = (req.params.os || '').toLowerCase();
559 const type = (req.query.type || 'installer').toLowerCase();
560 const osFiles = FILE_MAP[osName];
561 if (!osFiles) return res.status(400).send('Unsupported OS');
562 const filePath = osFiles[type];
563 if (!filePath || !fs.existsSync(filePath))
564 return res.status(404).send(`${type} not found`);
565 res.download(filePath, path.basename(filePath));
567 console.error('[DL TENANT] Failed:', err);
568 res.status(400).send('Invalid or expired token');
573 * @api {get} /api/download/checksum SHA-256 Checksum
574 * @apiName GetFileChecksum
575 * @apiGroup DownloadGroup
576 * @apiDescription Computes SHA-256 checksum for agent installer or binary. Uses FILE_MAP for OS/type lookup.
577 * Requires authentication.
578 * @apiParam (Query) {string} [os=windows] Operating system (windows, linux, mac)
579 * @apiParam (Query) {string} [type=agent] File type (agent, installer, etc.)
580 * @apiSuccess {string} os Operating system
581 * @apiSuccess {string} type File type
582 * @apiSuccess {string} checksum SHA-256 hash (hex)
583 * @apiError 400 Unsupported OS
584 * @apiError 404 File not found
585 * @apiError 500 Checksum computation failed
586 * @apiExample {curl} Example:
587 * curl "https://api.example.com/api/download/checksum?os=windows&type=agent" \
588 * -H "Authorization: Bearer YOUR_TOKEN"
589 * @apiExample {json} Success-Response (200):
593 * "checksum": "abc123def456..."
596// ----------------------
598// ----------------------
599router.get('/checksum', authenticateToken, (req, res) => {
601 const { os = 'windows', type = 'agent' } = req.query;
602 const osFiles = FILE_MAP[os.toLowerCase()];
603 if (!osFiles) return res.status(400).json({ error: 'Unsupported OS' });
604 const filePath = osFiles[type];
605 if (!filePath || !fs.existsSync(filePath))
606 return res.status(404).json({ error: 'File not found' });
607 const hash = crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
608 res.json({ os, type, checksum: hash });
610 console.error('[Checksum] Failed:', err);
611 res.status(500).json({ error: 'Checksum failed' });
616 * @api {get} /api/download/plugins/:pluginName Plugin File Serving
617 * @apiName GetPluginFile
618 * @apiGroup DownloadGroup
619 * @apiDescription Downloads plugin file from agent/plugins directory with access verification. Alternative to
620 * catch-all /:pluginName endpoint. Requires authentication.
621 * @apiParam {string} pluginName Plugin filename
622 * @apiSuccess (200) {Binary} file Plugin binary
623 * @apiError 404 Plugin not found
624 * @apiError 400 Plugin access error (permission denied)
625 * @apiExample {curl} Example:
626 * curl https://api.example.com/api/download/plugins/network-scanner.exe \
627 * -H "Authorization: Bearer YOUR_TOKEN" \
630// ----------------------
631// Plugin file serving
632// ----------------------
633router.get('/plugins/:pluginName', authenticateToken, (req, res) => {
634 const pluginName = req.params.pluginName;
635 const pluginDir = path.resolve(__dirname, '../../agent/plugins');
636 const pluginPath = path.join(pluginDir, pluginName);
637 console.log(`[Plugin DL] ${pluginName} requested`);
639 if (!fs.existsSync(pluginPath))
640 return res.status(404).send('Plugin not found');
641 fs.accessSync(pluginPath, fs.constants.R_OK);
642 res.download(pluginPath, pluginName);
644 console.error(`[Plugin DL] Error: ${pluginName}`, err);
645 res.status(400).send('Plugin access error');
649module.exports = router;