UNPKG

@bsv/overlay-express

Version:
916 lines 40.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const body_parser_1 = __importDefault(require("body-parser")); const overlay_1 = require("@bsv/overlay"); const sdk_1 = require("@bsv/sdk"); const knex_1 = __importDefault(require("knex")); const mongodb_1 = require("mongodb"); const makeUserInterface_js_1 = __importDefault(require("./makeUserInterface.js")); const DiscoveryServices = __importStar(require("@bsv/overlay-discovery-services")); const chalk_1 = __importDefault(require("chalk")); const util_1 = __importDefault(require("util")); const uuid_1 = require("uuid"); /** * In-memory migration source for Knex migrations. * Allows running migrations defined in code rather than files. */ class InMemoryMigrationSource { constructor(migrations) { this.migrations = migrations; } /** * Gets the list of migrations. * @param loadExtensions - Array of file extensions to filter by (not used here) * @returns Promise resolving to the array of migrations */ async getMigrations(loadExtensions) { return this.migrations; } /** * Gets the name of a migration. * @param migration - The migration object * @returns The name of the migration */ getMigrationName(migration) { return typeof migration.name === 'string' ? migration.name : `Migration at index ${this.migrations.indexOf(migration)}`; } /** * Gets the migration object. * @param migration - The migration object * @returns Promise resolving to the migration object */ async getMigration(migration) { return await Promise.resolve(migration); } } /** * OverlayExpress class provides an Express-based server for hosting Overlay Services. * It allows configuration of various components like databases, topic managers, and lookup services. * It encapsulates an Express application and provides methods to start the server. */ class OverlayExpress { /** * Constructs an instance of OverlayExpress. * @param name - The name of the service * @param privateKey - Private key used for signing advertisements * @param advertisableFQDN - The fully qualified domain name where this service is available. Does not include "https://". * @param adminToken - Optional. An administrative Bearer token used to protect admin routes. * If not provided, a random token will be generated at runtime. */ constructor(name, privateKey, advertisableFQDN, adminToken) { this.name = name; this.privateKey = privateKey; this.advertisableFQDN = advertisableFQDN; // Server port this.port = 3000; // Logger (defaults to console) this.logger = console; // Knex (SQL) database this.knex = {}; // Knex migrations to run this.migrationsToRun = []; // MongoDB database this.mongoDb = {}; // Network ('main' or 'test') this.network = 'main'; // If no custom ChainTracker is configured, default is a WhatsOnChain instance // (We keep a property for it, so we can pass it to Engine) this.chainTracker = new sdk_1.WhatsOnChain(this.network); // The Overlay Engine this.engine = {}; // Configured Topic Managers this.managers = {}; // Configured Lookup Services this.services = {}; // Enable GASP Sync // (We allow an on/off toggle, but also can do advanced custom sync config below) this.enableGASPSync = true; // ARC API Key this.arcApiKey = undefined; // Verbose request logging this.verboseRequestLogging = false; // Web UI configuration this.webUIConfig = {}; // Additional advanced engine config (these map to Engine constructor parameters). // Default to undefined or default values that are used in the Engine if not specified. this.engineConfig = {}; this.app = (0, express_1.default)(); this.logger.log(chalk_1.default.green.bold(`${name} constructed 🎉`)); this.adminToken = adminToken !== null && adminToken !== void 0 ? adminToken : (0, uuid_1.v4)(); // generate random if not provided } /** * Returns the current admin token in case you need to programmatically retrieve or display it. */ getAdminToken() { return this.adminToken; } /** * Configures the port on which the server will listen. * @param port - The port number */ configurePort(port) { this.port = port; this.logger.log(chalk_1.default.blue(`🌐 Server port set to ${port}`)); } /** * Configures the web user interface * @param config - Web UI configuration options */ configureWebUI(config) { this.webUIConfig = config; this.logger.log(chalk_1.default.blue('🖥️ Web UI has been configured.')); } /** * Configures the logger to be used by the server. * @param logger - A logger object (e.g., console) */ configureLogger(logger) { this.logger = logger; this.logger.log(chalk_1.default.blue('🔍 Logger has been configured.')); } /** * Configures the BSV Blockchain network to be used ('main' or 'test'). * By default, it re-initializes chainTracker as a WhatsOnChain for that network. * @param network - The network ('main' or 'test') */ configureNetwork(network) { this.network = network; this.chainTracker = new sdk_1.WhatsOnChain(this.network); this.logger.log(chalk_1.default.blue(`🔗 Network set to ${network}`)); } /** * Configures the ChainTracker to be used. * If 'scripts only' is used, it implies no full SPV chain tracking in the Engine. * @param chainTracker - An instance of ChainTracker or 'scripts only' */ configureChainTracker(chainTracker = new sdk_1.WhatsOnChain(this.network)) { this.chainTracker = chainTracker; this.logger.log(chalk_1.default.blue('🔗 ChainTracker has been configured.')); } /** * Configures the ARC API key. * @param apiKey - The ARC API key */ configureArcApiKey(apiKey) { this.arcApiKey = apiKey; this.logger.log(chalk_1.default.blue('🔑 ARC API key has been configured.')); } /** * Enables or disables GASP synchronization (high-level setting). * This is a broad toggle that can be overridden or customized through syncConfiguration. * @param enable - true to enable, false to disable */ configureEnableGASPSync(enable) { this.enableGASPSync = enable; this.logger.log(chalk_1.default.blue(`🔄 GASP synchronization ${enable ? 'enabled' : 'disabled'}.`)); } /** * Enables or disables verbose request logging. * @param enable - true to enable, false to disable */ configureVerboseRequestLogging(enable) { this.verboseRequestLogging = enable; this.logger.log(chalk_1.default.blue(`📝 Verbose request logging ${enable ? 'enabled' : 'disabled'}.`)); } /** * Configure Knex (SQL) database connection. * @param config - Knex configuration object, or MySQL connection string (e.g. mysql://overlayAdmin:overlay123@mysql:3306/overlay). */ async configureKnex(config) { if (typeof config === 'string') { config = { client: 'mysql2', connection: config }; } this.knex = (0, knex_1.default)(config); this.logger.log(chalk_1.default.blue('📦 Knex successfully configured.')); } /** * Configures the MongoDB database connection. * @param connectionString - MongoDB connection string */ async configureMongo(connectionString) { const mongoClient = new mongodb_1.MongoClient(connectionString); await mongoClient.connect(); const db = mongoClient.db(`${this.name}_lookup_services`); this.mongoDb = db; this.logger.log(chalk_1.default.blue('🍃 MongoDB successfully configured and connected.')); } /** * Configures a Topic Manager. * @param name - The name of the Topic Manager * @param manager - An instance of TopicManager */ configureTopicManager(name, manager) { this.managers[name] = manager; this.logger.log(chalk_1.default.blue(`🗂️ Configured topic manager ${name}`)); } /** * Configures a Lookup Service. * @param name - The name of the Lookup Service * @param service - An instance of LookupService */ configureLookupService(name, service) { this.services[name] = service; this.logger.log(chalk_1.default.blue(`🔍 Configured lookup service ${name}`)); } /** * Configures a Lookup Service using Knex (SQL) database. * @param name - The name of the Lookup Service * @param serviceFactory - A factory function that creates a LookupService instance using Knex */ configureLookupServiceWithKnex(name, serviceFactory) { this.ensureKnex(); const factoryResult = serviceFactory(this.knex); this.services[name] = factoryResult.service; this.migrationsToRun.push(...factoryResult.migrations); this.logger.log(chalk_1.default.blue(`🔍 Configured lookup service ${name} with Knex`)); } /** * Configures a Lookup Service using MongoDB. * @param name - The name of the Lookup Service * @param serviceFactory - A factory function that creates a LookupService instance using MongoDB */ configureLookupServiceWithMongo(name, serviceFactory) { this.ensureMongo(); this.services[name] = serviceFactory(this.mongoDb); this.logger.log(chalk_1.default.blue(`🔍 Configured lookup service ${name} with MongoDB`)); } /** * Advanced configuration method for setting or overriding any * Engine constructor parameters via an EngineConfig object. * * Example usage: * configureEngineParams({ * logTime: true, * throwOnBroadcastFailure: true, * overlayBroadcastFacilitator: new MyCustomFacilitator() * }) * * These fields will be respected when we finally build/configure the Engine * in the `configureEngine()` method below. */ configureEngineParams(params) { this.engineConfig = { ...this.engineConfig, ...params }; this.logger.log(chalk_1.default.blue('⚙️ Advanced Engine configuration params have been updated.')); } /** * Configures the Overlay Engine itself. * By default, auto-configures SHIP and SLAP unless autoConfigureShipSlap = false * Then it merges in any advanced engine config from `this.engineConfig`. * * @param autoConfigureShipSlap - Whether to auto-configure SHIP and SLAP services (default: true) */ async configureEngine(autoConfigureShipSlap = true) { var _a, _b, _c, _d, _e, _f, _g; this.ensureKnex(); if (autoConfigureShipSlap) { // Auto-configure SHIP and SLAP services this.configureTopicManager('tm_ship', new DiscoveryServices.SHIPTopicManager()); this.configureTopicManager('tm_slap', new DiscoveryServices.SLAPTopicManager()); this.configureLookupServiceWithMongo('ls_ship', (db) => new DiscoveryServices.SHIPLookupService(new DiscoveryServices.SHIPStorage(db))); this.configureLookupServiceWithMongo('ls_slap', (db) => new DiscoveryServices.SLAPLookupService(new DiscoveryServices.SLAPStorage(db))); } // Construct a default sync configuration, in case the user doesn't want GASP at all: let syncConfig = {}; if (!this.enableGASPSync) { // For each manager, disable sync for (const managerName of Object.keys(this.managers)) { syncConfig[managerName] = false; } } else { // If the user provided a syncConfiguration, use that. Otherwise default to an empty object. syncConfig = (_a = this.engineConfig.syncConfiguration) !== null && _a !== void 0 ? _a : {}; } // Build the actual Storage const storage = new overlay_1.KnexStorage(this.knex); // Include the KnexStorage migrations this.migrationsToRun = [...overlay_1.KnexStorageMigrations.default, ...this.migrationsToRun]; // Prepare broadcaster if arcApiKey is set let broadcaster; if (typeof this.arcApiKey === 'string') { broadcaster = new sdk_1.ARC( // We hard-code some ARC URLs for now, but we should make this configurable later. this.network === 'test' ? 'https://arc-test.taal.com' : 'https://arc.taal.com', { apiKey: this.arcApiKey }); } // Prepare advertiser if not set by the user let advertiser = this.engineConfig.advertiser; if (typeof advertiser === 'undefined') { try { advertiser = new DiscoveryServices.WalletAdvertiser(this.network, this.privateKey, this.network === 'test' // For now, we hard-code some storage servers. In the future, this needs to be configurable. ? 'https://staging-storage.babbage.systems' : 'https://storage.babbage.systems', // Until multiple protocols (like https+bsvauth+smf) are fully supported, HTTPS is the one to always use. `https://${this.advertisableFQDN}`); } catch (e) { this.logger.log(`Advertiser not initialized for FQDN ${this.advertisableFQDN} - SHIP and SLAP will be disabled.`); } } // Construct the Engine with any advanced config overrides. Fallback to defaults. this.engine = new overlay_1.Engine(this.managers, this.services, storage, // chainTracker typeof this.engineConfig.chainTracker !== 'undefined' ? this.engineConfig.chainTracker : this.chainTracker, // hostingURL `https://${this.advertisableFQDN}`, // shipTrackers this.network === 'test' ? ((_b = this.engineConfig.shipTrackers) !== null && _b !== void 0 ? _b : sdk_1.DEFAULT_TESTNET_SLAP_TRACKERS) : this.engineConfig.shipTrackers, // slapTrackers Array.isArray(this.engineConfig.slapTrackers) ? this.engineConfig.slapTrackers : this.network === 'test' ? sdk_1.DEFAULT_TESTNET_SLAP_TRACKERS : sdk_1.DEFAULT_SLAP_TRACKERS, // broadcaster broadcaster !== null && broadcaster !== void 0 ? broadcaster : this.engineConfig.broadcaster, // advertiser advertiser, // syncConfiguration syncConfig, // logTime (_c = this.engineConfig.logTime) !== null && _c !== void 0 ? _c : false, // logPrefix (_d = this.engineConfig.logPrefix) !== null && _d !== void 0 ? _d : '[OVERLAY_ENGINE] ', // throwOnBroadcastFailure (_e = this.engineConfig.throwOnBroadcastFailure) !== null && _e !== void 0 ? _e : false, // overlayBroadcastFacilitator (_f = this.engineConfig.overlayBroadcastFacilitator) !== null && _f !== void 0 ? _f : new sdk_1.HTTPSOverlayBroadcastFacilitator(), // logger this.logger, // suppressDefaultSyncAdvertisements (_g = this.engineConfig.suppressDefaultSyncAdvertisements) !== null && _g !== void 0 ? _g : true); this.logger.log(chalk_1.default.green('🚀 Engine has been configured.')); } /** * Ensures that Knex is configured. * @throws Error if Knex is not configured */ ensureKnex() { if (typeof this.knex === 'undefined') { throw new Error('You must configure your SQL database with the .configureKnex() method first!'); } } /** * Ensures that MongoDB is configured. * @throws Error if MongoDB is not configured */ ensureMongo() { if (typeof this.mongoDb === 'undefined') { throw new Error('You must configure your MongoDB connection with the .configureMongo() method first!'); } } /** * Ensures that the Overlay Engine is configured. * @throws Error if the Engine is not configured */ ensureEngine() { if (typeof this.engine === 'undefined') { throw new Error('You must configure your Overlay Services engine with the .configureEngine() method first!'); } } /** * Starts the Express server. * Sets up routes and begins listening on the configured port. */ async start() { var _a, _b, _c; this.ensureEngine(); this.ensureKnex(); const engine = this.engine; const knex = this.knex; this.app.use(body_parser_1.default.json({ limit: '1gb', type: 'application/json' })); this.app.use(body_parser_1.default.raw({ limit: '1gb', type: 'application/octet-stream' })); if (this.verboseRequestLogging) { this.app.use((req, res, next) => { const startTime = Date.now(); // Log incoming request details this.logger.log(chalk_1.default.magenta.bold(`📥 Incoming Request: ${String(req.method)} ${String(req.originalUrl)}`)); // Pretty-print headers this.logger.log(chalk_1.default.cyan('Headers:')); this.logger.log(util_1.default.inspect(req.headers, { colors: true, depth: null })); // Handle request body if (req.body != null && Object.keys(req.body).length > 0) { let bodyContent; let bodyString; if (typeof req.body === 'object') { bodyString = JSON.stringify(req.body, null, 2); } else if (Buffer.isBuffer(req.body)) { bodyString = req.body.toString('utf8'); } else { bodyString = String(req.body); } if (bodyString.length > 280) { bodyContent = chalk_1.default.yellow(`(Body too long to display, length: ${String(bodyString.length)} characters)`); } else { bodyContent = chalk_1.default.green(`Request Body:\n${String(bodyString)}`); } this.logger.log(bodyContent); } // Intercept the res.send method to log responses const originalSend = res.send; let responseBody; res.send = function (body) { responseBody = body; return originalSend.call(this, body); }; // Log outgoing response details after the response is finished res.on('finish', () => { const duration = Date.now() - startTime; this.logger.log(chalk_1.default.magenta.bold(`📤 Outgoing Response: ${String(req.method)} ${String(req.originalUrl)} - Status: ${String(res.statusCode)} - Duration: ${String(duration)}ms`)); this.logger.log(chalk_1.default.cyan('Response Headers:')); this.logger.log(util_1.default.inspect(res.getHeaders(), { colors: true, depth: null })); // Handle response body if (responseBody != null) { let bodyContent; let bodyString; if (typeof responseBody === 'object') { bodyString = JSON.stringify(responseBody, null, 2); } else if (Buffer.isBuffer(responseBody)) { bodyString = responseBody.toString('utf8'); } else if (typeof responseBody === 'string') { bodyString = responseBody; } else { bodyString = String(responseBody); } if (bodyString.length > 280) { bodyContent = chalk_1.default.yellow(`(Response body too long to display, length: ${String(bodyString.length)} characters)`); } else { bodyContent = chalk_1.default.green(`Response Body:\n${String(bodyString)}`); } this.logger.log(bodyContent); } }); next(); }); } // Enable CORS this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', '*'); res.header('Access-Control-Allow-Methods', '*'); res.header('Access-Control-Expose-Headers', '*'); res.header('Access-Control-Allow-Private-Network', 'true'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // Serve a static documentation site or user interface this.app.get('/', (req, res) => { res.set('content-type', 'text/html'); res.send((0, makeUserInterface_js_1.default)(this.webUIConfig)); }); // List hosted topic managers and lookup services this.app.get('/listTopicManagers', (_, res) => { ; (async () => { try { const result = await engine.listTopicManagers(); return res.status(200).json(result); } catch (error) { return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { // This catch is for any unforeseen errors in the async IIFE itself res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); this.app.get('/listLookupServiceProviders', (_, res) => { ; (async () => { try { const result = await engine.listLookupServiceProviders(); return res.status(200).json(result); } catch (error) { return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); // Host documentation for the services this.app.get('/getDocumentationForTopicManager', (req, res) => { ; (async () => { try { const manager = req.query.manager; const result = await engine.getDocumentationForTopicManager(manager); res.setHeader('Content-Type', 'text/markdown'); return res.status(200).send(result); } catch (error) { return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); this.app.get('/getDocumentationForLookupServiceProvider', (req, res) => { ; (async () => { try { const lookupService = req.query.lookupService; const result = await engine.getDocumentationForLookupServiceProvider(lookupService); res.setHeader('Content-Type', 'text/markdown'); return res.status(200).send(result); } catch (error) { return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); // Submit transactions and facilitate lookup requests this.app.post('/submit', (req, res) => { ; (async () => { try { // Parse out the topics and construct the tagged BEEF const topicsHeader = req.headers['x-topics']; const includesOffChain = req.headers['x-includes-off-chain-values'] === 'true'; if (typeof topicsHeader !== 'string') { throw new Error('Missing x-topics header'); } const topics = JSON.parse(topicsHeader); let offChainValues; let beef = Array.from(req.body); if (includesOffChain) { const r = new sdk_1.Utils.Reader(beef); const l = r.readVarIntNum(); beef = r.read(l); offChainValues = r.read(); } const taggedBEEF = { beef, topics, offChainValues }; // Using a callback function, we can return once the STEAK is ready let responseSent = false; const steak = await engine.submit(taggedBEEF, (steak) => { responseSent = true; return res.status(200).json(steak); }, 'current-tx', offChainValues); if (!responseSent) { res.status(200).json(steak); } } catch (error) { console.error(chalk_1.default.red('❌ Error in /submit:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); this.app.post('/lookup', (req, res) => { ; (async () => { try { const result = await engine.lookup(req.body); return res.status(200).json(result); } catch (error) { console.error(chalk_1.default.red('❌ Error in /lookup:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); // ARC ingest route (only if we have an ARC API key) if (typeof this.arcApiKey === 'string' && this.arcApiKey.length > 0) { this.app.post('/arc-ingest', (req, res) => { ; (async () => { try { const { txid, merklePath: merklePathHex, blockHeight } = req.body; const merklePath = sdk_1.MerklePath.fromHex(merklePathHex); await engine.handleNewMerkleProof(txid, merklePath, blockHeight); return res.status(200).json({ status: 'success', message: 'Transaction status updated' }); } catch (error) { console.error(chalk_1.default.red('❌ Error in /arc-ingest:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); } else { this.logger.warn(chalk_1.default.yellow('⚠️ Disabling ARC because no ARC API key was provided.')); } // GASP sync routes if enabled if (this.enableGASPSync) { this.app.post('/requestSyncResponse', (req, res) => { ; (async () => { try { const topic = req.headers['x-bsv-topic']; const response = await engine.provideForeignSyncResponse(req.body, topic); return res.status(200).json(response); } catch (error) { console.error(chalk_1.default.red('❌ Error in /requestSyncResponse:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); this.app.post('/requestForeignGASPNode', (req, res) => { ; (async () => { try { const { graphID, txid, outputIndex } = req.body; const response = await engine.provideForeignGASPNode(graphID, txid, outputIndex); return res.status(200).json(response); } catch (error) { console.error(chalk_1.default.red('❌ Error in /requestForeignGASPNode:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); } else { this.logger.warn(chalk_1.default.yellow('⚠️ GASP sync is disabled.')); } /** * ============== ADMIN ROUTES ============== * These routes expose advanced engine operations * and require a valid Bearer token for access. */ /** * Middleware for checking the admin bearer token. */ const checkAdminAuth = (req, res, next) => { const authHeader = req.headers.authorization; if (typeof authHeader !== 'string' || !authHeader.startsWith('Bearer ')) { res.status(401).json({ status: 'error', message: 'Unauthorized: Missing Bearer token' }); return; } const token = authHeader.substring('Bearer '.length); if (token !== this.adminToken) { res.status(403).json({ status: 'error', message: 'Forbidden: Invalid Bearer token' }); return; } next(); }; /** * Admin route to manually sync advertisements, calling `engine.syncAdvertisements()`. */ this.app.post('/admin/syncAdvertisements', checkAdminAuth, (req, res) => { ; (async () => { try { await engine.syncAdvertisements(); return res.status(200).json({ status: 'success', message: 'Advertisements synced successfully' }); } catch (error) { console.error(chalk_1.default.red('❌ Error in /admin/syncAdvertisements:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); /** * Admin route to manually start GASP sync, calling `engine.startGASPSync()`. */ this.app.post('/admin/startGASPSync', checkAdminAuth, (req, res) => { ; (async () => { try { await engine.startGASPSync(); return res.status(200).json({ status: 'success', message: 'GASP sync started and completed' }); } catch (error) { console.error(chalk_1.default.red('❌ Error in /admin/startGASPSync:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); /** * Admin route to evict an outpoint, either from all services or a specific one. */ this.app.post('/admin/evictOutpoint', checkAdminAuth, (req, res) => { ; (async () => { try { if (typeof req.body.service === 'string') { const service = engine.lookupServices[req.body.service]; await service.outputEvicted(req.body.txid, req.body.outputIndex); } else { const services = Object.values(engine.lookupServices); for (let i = 0; i < services.length; i++) { try { await services[i].outputEvicted(req.body.txid, req.body.outputIndex); } catch { continue; } } } return res.status(200).json({ status: 'success', message: 'Outpoint evicted' }); } catch (error) { console.error(chalk_1.default.red('❌ Error in /admin/evictOutpoint:'), error); return res.status(400).json({ status: 'error', message: error instanceof Error ? error.message : 'An unknown error occurred' }); } })().catch(() => { res.status(500).json({ status: 'error', message: 'Unexpected error' }); }); }); // Automatically handle migrations const migrationSource = new InMemoryMigrationSource(this.migrationsToRun); const result = await knex.migrate.latest({ migrationSource }); this.logger.log(chalk_1.default.green('🔄 Knex migrations run'), result); // 404 handler for all other routes this.app.use((req, res) => { this.logger.log(chalk_1.default.red('❌ 404 Not Found:'), req.url); res.status(404).json({ status: 'error', code: 'ERR_ROUTE_NOT_FOUND', description: 'Route not found.' }); }); // The legacy Ninja advertiser has a setLookupEngine method. if (((_a = this.engine) === null || _a === void 0 ? void 0 : _a.advertiser) instanceof DiscoveryServices.WalletAdvertiser) { this.logger.log(chalk_1.default.cyan(`${this.name} will now advertise with SHIP and SLAP as appropriate at FQDN: ${this.advertisableFQDN}`)); await this.engine.advertiser.init(); } // Log some info about topic managers and services const numTopicManagers = Object.keys(this.managers).length; const numLookupServices = Object.keys(this.services).length; this.logger.log(chalk_1.default.blue(`Topic Managers: ${numTopicManagers}`)); this.logger.log(chalk_1.default.blue(`Lookup Services: ${numLookupServices}`)); // Attempt to sync advertisements try { await ((_b = this.engine) === null || _b === void 0 ? void 0 : _b.syncAdvertisements()); } catch (e) { this.logger.log(chalk_1.default.red('❌ Error syncing advertisements:'), e); } // Attempt to do GASP sync if enabled if (this.enableGASPSync) { try { this.logger.log(chalk_1.default.green('Starting GASP sync...')); await ((_c = this.engine) === null || _c === void 0 ? void 0 : _c.startGASPSync()); this.logger.log(chalk_1.default.green('GASP sync complete!')); } catch (e) { console.error(chalk_1.default.red('❌ Failed to GASP sync'), e); } } else { this.logger.log(chalk_1.default.yellow(`${this.name} will not sync because GASP has been disabled.`)); } // Start listening on the configured port this.app.listen(this.port, () => { this.logger.log(chalk_1.default.green.bold(`🎧 ${this.name} is ready and listening on local port ${this.port}`)); }); } } exports.default = OverlayExpress; //# sourceMappingURL=OverlayExpress.js.map