@bsv/overlay-express
Version:
BSV Blockchain Overlay Express
916 lines • 40.8 kB
JavaScript
"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