UNPKG

@bsv/overlay-express

Version:
215 lines โ€ข 9.24 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JanitorService = void 0; const chalk_1 = __importDefault(require("chalk")); /** * JanitorService runs a single pass of health checks on SHIP and SLAP outputs. * It validates domain names and checks /health endpoints to ensure services are operational. * * When a service is down, it increments a "down" counter. When healthy, it decrements. * If the down counter reaches HOST_DOWN_REVOKE_SCORE, it deletes the output from the database. * * This service is designed to be run periodically via external schedulers (e.g., cron, docker-compose). */ class JanitorService { constructor(config) { var _a, _b, _c; this.mongoDb = config.mongoDb; this.logger = (_a = config.logger) !== null && _a !== void 0 ? _a : console; this.requestTimeoutMs = (_b = config.requestTimeoutMs) !== null && _b !== void 0 ? _b : 10000; // Default: 10 seconds this.hostDownRevokeScore = (_c = config.hostDownRevokeScore) !== null && _c !== void 0 ? _c : 3; } /** * Runs a single pass of health checks on all SHIP and SLAP outputs */ async run() { this.logger.log(chalk_1.default.blue('๐Ÿงน Running janitor health checks...')); try { // Check SHIP outputs await this.checkTopicOutputs('shipRecords'); // Check SLAP outputs await this.checkTopicOutputs('slapRecords'); this.logger.log(chalk_1.default.green('๐Ÿงน Janitor health checks completed')); } catch (error) { this.logger.error(chalk_1.default.red('โŒ Error during health checks:'), error); throw error; } } /** * Checks all outputs for a specific topic */ async checkTopicOutputs(collectionName) { try { const collection = this.mongoDb.collection(collectionName); const outputs = await collection.find({}).toArray(); this.logger.log(chalk_1.default.cyan(`๐Ÿ” Checking ${outputs.length} ${collectionName.toUpperCase()} outputs...`)); for (const output of outputs) { await this.checkOutput(output, collection); } } catch (error) { this.logger.error(chalk_1.default.red(`โŒ Error checking ${collectionName.toUpperCase()} outputs:`), error); } } /** * Checks a single output for health */ async checkOutput(output, collection) { try { // Extract the URL from the output const url = this.extractURLFromOutput(output); if (url === null) { return; // Skip if no valid URL } // Validate domain if (!this.isValidDomain(url)) { this.logger.log(chalk_1.default.yellow(`โš ๏ธ Invalid domain for output ${String(output.txid)}:${String(output.outputIndex)}: ${url}`)); await this.handleUnhealthyOutput(output, collection); return; } // Check health endpoint const isHealthy = await this.checkHealthEndpoint(url); if (isHealthy) { await this.handleHealthyOutput(output, collection); } else { await this.handleUnhealthyOutput(output, collection); } } catch (error) { this.logger.error(chalk_1.default.red(`โŒ Error checking output ${String(output.txid)}:${String(output.outputIndex)}:`), error); await this.handleUnhealthyOutput(output, collection).catch(() => { }); } } /** * Extracts URL from output record */ extractURLFromOutput(output) { try { // SHIP and SLAP outputs typically have a domain field or URL field // Check for common field names if (typeof output.domain === 'string') { return output.domain; } if (typeof output.url === 'string') { return output.url; } if (typeof output.serviceURL === 'string') { return output.serviceURL; } // SLAP outputs may have a protocols array with URLs if (Array.isArray(output.protocols) && output.protocols.length > 0) { const httpsProtocol = output.protocols.find((p) => typeof p === 'string' && p.startsWith('https://')); if (httpsProtocol !== undefined) { return httpsProtocol; } } return null; } catch { return null; } } /** * Validates if a string is a valid domain name */ isValidDomain(url) { try { // Parse the URL const parsedURL = new URL(url.startsWith('http') ? url : `https://${url}`); // Check if hostname is valid const hostname = parsedURL.hostname; // Basic domain validation const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i; const localhostRegex = /^localhost$/i; const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; return domainRegex.test(hostname) || localhostRegex.test(hostname) || ipv4Regex.test(hostname); } catch { return false; } } /** * Checks the /health endpoint of a service */ async checkHealthEndpoint(url) { try { // Ensure URL has protocol const fullURL = url.startsWith('http') ? url : `https://${url}`; const healthURL = new URL('/health', fullURL).toString(); // Create AbortController for timeout const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs); try { const response = await fetch(healthURL, { method: 'GET', signal: controller.signal, headers: { Accept: 'application/json' } }); clearTimeout(timeout); if (!response.ok) { return false; } const data = await response.json(); return (data === null || data === void 0 ? void 0 : data.status) === 'ok'; } catch (error) { clearTimeout(timeout); if (error.name === 'AbortError') { this.logger.log(chalk_1.default.yellow(`โฑ๏ธ Health check timeout for ${healthURL}`)); } return false; } } catch (error) { return false; } } /** * Handles a healthy output by decrementing its down counter */ async handleHealthyOutput(output, collection) { try { const currentDown = typeof output.down === 'number' ? output.down : 0; if (currentDown > 0) { // Decrement the down counter await collection.updateOne({ _id: output._id }, { $inc: { down: -1 } }); this.logger.log(chalk_1.default.green(`โœ… Output ${String(output.txid)}:${String(output.outputIndex)} is healthy (down: ${currentDown} -> ${currentDown - 1})`)); } } catch (error) { this.logger.error(chalk_1.default.red(`โŒ Error handling healthy output ${String(output.txid)}:${String(output.outputIndex)}:`), error); } } /** * Handles an unhealthy output by incrementing its down counter and deleting if threshold is reached */ async handleUnhealthyOutput(output, collection) { try { const currentDown = typeof output.down === 'number' ? output.down : 0; const newDown = currentDown + 1; this.logger.log(chalk_1.default.yellow(`โš ๏ธ Output ${String(output.txid)}:${String(output.outputIndex)} is unhealthy (down: ${currentDown} -> ${newDown})`)); // Check if we should delete the record if (newDown >= this.hostDownRevokeScore) { this.logger.log(chalk_1.default.red(`๐Ÿšซ Deleting output ${String(output.txid)}:${String(output.outputIndex)} (down count: ${newDown} >= ${this.hostDownRevokeScore})`)); await collection.deleteOne({ _id: output._id }); this.logger.log(chalk_1.default.green(`โœ… Successfully deleted output ${String(output.txid)}:${String(output.outputIndex)}`)); } else { // Increment the down counter await collection.updateOne({ _id: output._id }, { $inc: { down: 1 } }); } } catch (error) { this.logger.error(chalk_1.default.red(`โŒ Error handling unhealthy output ${String(output.txid)}:${String(output.outputIndex)}:`), error); } } } exports.JanitorService = JanitorService; //# sourceMappingURL=JanitorService.js.map