"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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __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 });
exports.ApiServer = void 0;
const async_exit_hook_1 = __importDefault(require("async-exit-hook"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const tls_1 = __importDefault(require("tls"));
const http_1 = require("http");
const https_1 = require("https");
const run_time_error_cjs_1 = require("run-time-error-cjs");
const semver_1 = require("semver");
const lmify_1 = __importDefault(require("lmify"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const express_http_proxy_1 = __importDefault(require("express-http-proxy"));
const grpc_js_1 = require("@grpc/grpc-js");
const grpc_js_2 = require("@grpc/grpc-js");
const connect_express_1 = require("@connectrpc/connect-express");
const fastify_1 = require("fastify");
const connect_fastify_1 = require("@connectrpc/connect-fastify");
const express_1 = __importDefault(require("express"));
const compression_1 = __importDefault(require("compression"));
const body_parser_1 = __importDefault(require("body-parser"));
const cors_1 = __importDefault(require("cors"));
const socket_io_1 = require("socket.io");
const socketio_jwt_1 = require("@thream/socketio-jwt");
const cactus_core_api_1 = require("@hyperledger/cactus-core-api");
const cactus_core_1 = require("@hyperledger/cactus-core");
const cactus_core_2 = require("@hyperledger/cactus-core");
const cactus_common_1 = require("@hyperledger/cactus-common");
const openapi_json_1 = __importDefault(require("../json/openapi.json"));
const prometheus_exporter_1 = require("./prometheus-exporter/prometheus-exporter");
const authorizer_factory_1 = require("./authzn/authorizer-factory");
const typescript_axios_1 = require("./generated/openapi/typescript-axios");
const watch_healthcheck_v1_endpoint_1 = require("./web-services/watch-healthcheck-v1-endpoint");
const default_service = __importStar(require("./generated/proto/protoc-gen-ts/services/default_service"));
const grpc_server_api_server_1 = require("./web-services/grpc/grpc-server-api-server");
const determine_address_family_1 = require("./common/determine-address-family");
const get_open_api_spec_v1_endpoint_1 = require("./web-services/get-open-api-spec-v1-endpoint");
class ApiServer {
    get className() {
        return ApiServer.CLASS_NAME;
    }
    constructor(options) {
        this.options = options;
        if (!options) {
            throw new Error(`ApiServer#ctor options was falsy`);
        }
        if (!options.config) {
            throw new Error(`ApiServer#ctor options.config was falsy`);
        }
        this.boundGrpcHostPort = "127.0.0.1:-1";
        this.enableShutdownHook = cactus_common_1.Bools.isBooleanStrict(options.config.enableShutdownHook)
            ? options.config.enableShutdownHook
            : true;
        if (this.enableShutdownHook) {
            (0, async_exit_hook_1.default)(() => this.shutdown());
        }
        if (this.options.httpServerApi) {
            this.httpServerApi = this.options.httpServerApi;
        }
        else if (this.options.config.apiTlsEnabled) {
            this.httpServerApi = (0, https_1.createServer)({
                key: this.options.config.apiTlsKeyPem,
                cert: this.options.config.apiTlsCertPem,
            });
        }
        else {
            this.httpServerApi = (0, http_1.createServer)();
        }
        if (this.options.config.cockpitEnabled) {
            if (this.options.httpServerCockpit) {
                this.httpServerCockpit = this.options.httpServerCockpit;
            }
            else if (this.options.config.cockpitTlsEnabled) {
                this.httpServerCockpit = (0, https_1.createServer)({
                    key: this.options.config.cockpitTlsKeyPem,
                    cert: this.options.config.cockpitTlsCertPem,
                });
            }
            else {
                this.httpServerCockpit = (0, http_1.createServer)();
            }
        }
        this.crpcServer =
            this.options.crpcServer ||
                (0, fastify_1.fastify)({ http2: true, forceCloseConnections: true });
        this.grpcServer = this.options.grpcServer || new grpc_js_1.Server({});
        this.wsApi = new socket_io_1.Server();
        this.expressApi = (0, express_1.default)();
        this.expressCockpit = (0, express_1.default)();
        if (this.options.prometheusExporter) {
            this.prometheusExporter = this.options.prometheusExporter;
        }
        else {
            this.prometheusExporter = new prometheus_exporter_1.PrometheusExporter({
                pollingIntervalInMin: 1,
            });
        }
        this.prometheusExporter.startMetricsCollection();
        this.prometheusExporter.setTotalPluginImports(this.getPluginImportsCount());
        this.log = cactus_common_1.LoggerProvider.getOrCreate({
            label: "api-server",
            level: options.config.logLevel,
        });
        if (this.enableShutdownHook) {
            (0, async_exit_hook_1.default)((onHookDone) => {
                this.log.info("Starting async-exit-hook for cmd-api-server ...");
                this.shutdown()
                    .catch((ex) => {
                    this.log.warn("Failed async-exit-hook for cmd-api-server", ex);
                    throw ex;
                })
                    .finally(() => {
                    this.log.info("Concluded async-exit-hook for cmd-api-server ...");
                    onHookDone();
                });
                this.log.info("Started async-exit-hook for cmd-api-server OK");
            });
            this.log.info("Registered async-exit-hook for cmd-api-server shutdown.");
        }
        const defaultPluginsPath = path_1.default.join(os_1.default.tmpdir(), "org", "hyperledger", "cactus", "plugins");
        const { pluginsPath } = Object.assign(Object.assign({ pluginsPath: defaultPluginsPath }, JSON.parse(this.options.config.pluginManagerOptionsJson)), this.options.pluginManagerOptions);
        this.pluginsPath = pluginsPath;
        this.log.debug("pluginsPath: %o", pluginsPath);
    }
    getPrometheusExporter() {
        return this.prometheusExporter;
    }
    async getPrometheusExporterMetrics() {
        this.prometheusExporter.setTotalPluginImports(this.getPluginImportsCount());
        const res = await this.prometheusExporter.getPrometheusMetrics();
        this.log.debug(`getPrometheusExporterMetrics() response: %o`, res);
        return res;
    }
    getPluginImportsCount() {
        var _a;
        return ((_a = this.pluginRegistry) === null || _a === void 0 ? void 0 : _a.plugins.length) || 0;
    }
    async start() {
        this.checkNodeVersion();
        const tlsMaxVersion = this.options.config.tlsDefaultMaxVersion;
        this.log.info("Setting tls.DEFAULT_MAX_VERSION to %s...", tlsMaxVersion);
        tls_1.default.DEFAULT_MAX_VERSION = tlsMaxVersion;
        try {
            const { cockpitTlsEnabled, apiTlsEnabled } = this.options.config;
            let addressInfoCockpit;
            if (this.options.config.cockpitEnabled) {
                addressInfoCockpit = await this.startCockpitFileServer();
            }
            const addressInfoApi = await this.startApiServer();
            const addressInfoGrpc = await this.startGrpcServer();
            const { addressInfoCrpc, crpcUrl } = await this.startCrpcServer();
            this.log.debug("Cacti CRPC reachable %s", crpcUrl);
            {
                const { port, address } = addressInfoGrpc;
                const grpcUrl = `${address}:${port}`;
                this.log.info(`Cactus gRPC reachable ${grpcUrl}`);
            }
            {
                const { apiHost: host } = this.options.config;
                const { port } = addressInfoApi;
                const protocol = apiTlsEnabled ? "https:" : "http:";
                const httpUrl = `${protocol}//${host}:${port}`;
                this.log.info(`Cactus API reachable ${httpUrl}`);
            }
            if (this.options.config.cockpitEnabled) {
                const { cockpitHost: host } = this.options.config;
                const { port } = addressInfoCockpit;
                const protocol = cockpitTlsEnabled ? "https:" : "http:";
                const httpUrl = `${protocol}//${host}:${port}`;
                this.log.info(`Cactus Cockpit reachable ${httpUrl}`);
            }
            return {
                addressInfoCockpit,
                addressInfoApi,
                addressInfoGrpc,
                addressInfoCrpc,
            };
        }
        catch (ex1) {
            const context = "Failed to start ApiServer";
            this.log.error(context, ex1);
            this.log.error(`Attempting shutdown...`);
            try {
                await this.shutdown();
                this.log.info(`Server shut down after crash OK`);
            }
            catch (ex2) {
                this.log.error(ApiServer.E_POST_CRASH_SHUTDOWN, ex2);
            }
            throw (0, cactus_common_1.newRex)(context, ex1);
        }
    }
    /**
     * Verifies that the currently running NodeJS process is at least of a certain
     * NodeJS version as specified by the configuration.
     *
     * @throws {Error} if the version contraint is not satisfied by the runtime.
     */
    checkNodeVersion(currentVersion = process.version) {
        if ((0, semver_1.gte)(this.options.config.minNodeVersion, currentVersion)) {
            const msg = `ApiServer#checkNodeVersion() detected NodeJS ` +
                `v${process.version} that is outdated as per the configuration. ` +
                `If you must run on this NodeJS version you can override the minimum ` +
                `acceptable version via config parameters of the API server. ` +
                `Though doing so may lead to vulnerabilities in your deployment. ` +
                `You've been warned.`;
            throw new Error(msg);
        }
    }
    getHttpServerApi() {
        return this.httpServerApi;
    }
    getHttpServerCockpit() {
        return this.httpServerCockpit;
    }
    async getOrInitPluginRegistry() {
        try {
            if (!this.pluginRegistry) {
                if (!this.options.pluginRegistry) {
                    this.log.info(`getOrInitPluginRegistry() initializing a new one...`);
                    this.pluginRegistry = await this.initPluginRegistry();
                }
                else {
                    this.log.info(`getOrInitPluginRegistry() re-using injected one...`);
                    this.pluginRegistry = this.options.pluginRegistry;
                }
            }
            await this.prometheusExporter.setTotalPluginImports(await this.getPluginImportsCount());
            return this.pluginRegistry;
        }
        catch (ex) {
            this.pluginRegistry = new cactus_core_1.PluginRegistry({ plugins: [] });
            const context = "Failed to init PluginRegistry";
            this.log.debug(context, ex);
            throw (0, cactus_common_1.newRex)(context, ex);
        }
    }
    async initPluginRegistry(req) {
        const { pluginRegistry = new cactus_core_1.PluginRegistry({ plugins: [] }) } = req || {};
        const { plugins } = this.options.config;
        this.log.info(`Instantiated empty registry, invoking plugin factories...`);
        for (const pluginImport of plugins) {
            const plugin = await this.instantiatePlugin(pluginImport, pluginRegistry);
            pluginRegistry.add(plugin);
        }
        return pluginRegistry;
    }
    async instantiatePlugin(pluginImport, registry) {
        const fnTag = `${this.className}#instantiatePlugin`;
        const { logLevel } = this.options.config;
        const { packageName, options } = pluginImport;
        this.log.info(`Creating plugin from package: ${packageName}`, options);
        const pluginOptions = Object.assign(Object.assign({}, options), { logLevel, pluginRegistry: registry });
        try {
            if (pluginImport.action == cactus_core_api_1.PluginImportAction.Install) {
                await this.installPluginPackage(pluginImport);
            }
            else {
                this.log.info(`The installation of the plugin package ${packageName} was skipped due to the configuration flag action`);
            }
            const packagePath = path_1.default.join(this.pluginsPath, options.instanceId, "node_modules", packageName);
            this.log.debug("Package path: %o", packagePath);
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            const pluginPackage = require(/* webpackIgnore: true */ packagePath);
            const createPluginFactory = pluginPackage.createPluginFactory;
            const pluginFactoryOptions = {
                pluginImportType: pluginImport.type,
            };
            const pluginFactory = await createPluginFactory(pluginFactoryOptions);
            const plugin = await pluginFactory.create(pluginOptions);
            // need to invoke the i-cactus-plugin onPluginInit functionality here before plugin registry can be used further
            await plugin.onPluginInit();
            return plugin;
        }
        catch (ex) {
            const context = `${fnTag} failed instantiating plugin '${packageName}' with the instanceId '${options.instanceId}'`;
            this.log.debug(context, ex);
            throw (0, cactus_common_1.newRex)(context, ex);
        }
    }
    async installPluginPackage(pluginImport) {
        const fnTag = `ApiServer#installPluginPackage()`;
        const pkgName = pluginImport.options.packageSrc
            ? pluginImport.options.packageSrc
            : pluginImport.packageName;
        const instanceId = pluginImport.options.instanceId;
        const pluginPackageDir = path_1.default.join(this.pluginsPath, instanceId);
        // version of the npm package
        const pluginVersion = pluginImport.options.version
            ? "@".concat(pluginImport.options.version)
            : "";
        try {
            await fs_extra_1.default.mkdirp(pluginPackageDir);
            this.log.debug(`${pkgName} plugin package dir: %o`, pluginPackageDir);
        }
        catch (ex) {
            const context = "Could not create plugin installation directory, check the file-system permissions.";
            throw (0, cactus_common_1.newRex)(context, ex);
        }
        try {
            lmify_1.default.setPackageManager("npm");
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-expect-error
            lmify_1.default.setRootDir(pluginPackageDir);
            this.log.debug(`Installing ${pkgName} for plugin import`, pluginImport);
            const out = await lmify_1.default.install([
                pkgName.concat(pluginVersion), // empty if no version was specified
                "--production",
                "--audit=false",
                "--progress=false",
                "--fund=false",
                `--prefix=${pluginPackageDir}`,
                // "--ignore-workspace-root-check",
            ]);
            this.log.debug("%o install result: %o", pkgName, out);
            if ((out === null || out === void 0 ? void 0 : out.exitCode) && out.exitCode !== 0) {
                const eMsg = "Non-zero exit code returned by lmify.install() indicating that the underlying npm install OS process had encountered a problem:";
                throw (0, cactus_common_1.newRex)(eMsg, out);
            }
            this.log.info(`Installed ${pkgName} OK`);
        }
        catch (ex) {
            const context = `${fnTag} failed installing plugin '${pkgName}`;
            this.log.debug(ex, context);
            throw (0, cactus_common_1.newRex)(context, ex);
        }
    }
    async shutdown() {
        var _a, _b;
        this.log.info(`Shutting down API server ...`);
        const registry = await this.getOrInitPluginRegistry();
        const webServicesShutdown = registry
            .getPlugins()
            .filter((pluginInstance) => (0, cactus_core_api_1.isIPluginWebService)(pluginInstance))
            .map((pluginInstance) => {
            return pluginInstance.shutdown();
        });
        if (this.wsApi) {
            this.log.info(`Disconnecting SocketIO connections...`);
            this.wsApi.disconnectSockets(true);
            this.log.info(`SocketIO connections disconnect OK`);
        }
        this.log.info(`Stopping ${webServicesShutdown.length} WS plugin(s)...`);
        await Promise.all(webServicesShutdown);
        this.log.info(`Stopped ${webServicesShutdown.length} WS plugin(s) OK`);
        if ((_a = this.httpServerApi) === null || _a === void 0 ? void 0 : _a.listening) {
            this.log.info(`Closing Cacti HTTP server of the API...`);
            await cactus_common_1.Servers.shutdown(this.httpServerApi);
            this.log.info(`Close HTTP server of the API OK`);
        }
        if ((_b = this.httpServerCockpit) === null || _b === void 0 ? void 0 : _b.listening) {
            this.log.info(`Closing Cacti HTTP server of the cockpit ...`);
            await cactus_common_1.Servers.shutdown(this.httpServerCockpit);
            this.log.info(`Close HTTP server of the cockpit OK`);
        }
        if (this.crpcServer) {
            const fastifyPlugins = this.crpcServer.printPlugins();
            this.log.info("Fastify plugin list: %o", fastifyPlugins);
            this.log.info(`Closing Cacti CRPC HTTP server ...`);
            await this.crpcServer.close();
            this.log.info(`Closed Cacti CRPC HTTP server OK`);
        }
        if (this.grpcServer) {
            await new Promise((resolve, reject) => {
                this.log.info(`Draining Cacti gRPC server ...`);
                this.grpcServer.drain(this.boundGrpcHostPort, 5000);
                this.log.info(`Drained Cacti gRPC server OK`);
                this.log.info(`Trying to shut down Cacti gRPC server ...`);
                this.grpcServer.tryShutdown((ex) => {
                    if (ex) {
                        const eMsg = "Failed to shut down gRPC server of the Cacti API server.";
                        this.log.debug(eMsg, ex);
                        reject((0, cactus_common_1.newRex)(eMsg, ex));
                    }
                    else {
                        this.log.info(`Shut down Cacti gRPC server OK`);
                        resolve();
                    }
                });
            });
        }
    }
    async startCockpitFileServer() {
        var _a, _b, _c;
        const { expressCockpit: app } = this;
        const cockpitWwwRoot = this.options.config.cockpitWwwRoot;
        this.log.info(`wwwRoot: ${cockpitWwwRoot}`);
        const resolvedWwwRoot = path_1.default.resolve(process.cwd(), cockpitWwwRoot);
        this.log.info(`resolvedWwwRoot: ${resolvedWwwRoot}`);
        const resolvedIndexHtml = path_1.default.resolve(resolvedWwwRoot + "/index.html");
        this.log.info(`resolvedIndexHtml: ${resolvedIndexHtml}`);
        const cockpitCorsDomainCsv = this.options.config.cockpitCorsDomainCsv;
        const allowedDomains = cockpitCorsDomainCsv.split(",");
        const corsMiddleware = this.createCorsMiddleware(allowedDomains);
        const { apiHost, apiPort, cockpitApiProxyRejectUnauthorized: rejectUnauthorized, } = this.options.config;
        const protocol = this.options.config.apiTlsEnabled ? "https:" : "http:";
        const apiHttpUrl = `${protocol}//${apiHost}:${apiPort}`;
        const apiProxyMiddleware = (0, express_http_proxy_1.default)(apiHttpUrl, {
            // preserve the path whatever it was. Without this the proxy just uses /
            proxyReqPathResolver: (srcReq) => srcReq.originalUrl,
            proxyReqOptDecorator: (proxyReqOpts, srcReq) => {
                const { originalUrl: thePath } = srcReq;
                const srcHost = srcReq.header("host");
                const { host: destHostname, port: destPort } = proxyReqOpts;
                const destHost = `${destHostname}:${destPort}`;
                this.log.debug(`PROXY ${srcHost} => ${destHost} :: ${thePath}`);
                // make sure self signed certs are accepted if it was configured as such by the user
                proxyReqOpts.rejectUnauthorized =
                    rejectUnauthorized;
                return proxyReqOpts;
            },
        });
        const { rateLimit } = await Promise.resolve().then(() => __importStar(require("express-rate-limit")));
        const rateLimiterIndexHtml = rateLimit({
            windowMs: 15 * 60 * 1000, // 15 minutes
            max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
            standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
            legacyHeaders: false, // Disable the `X-RateLimit-*` headers
        });
        const middlewareIndexHtml = (_, res) => res.sendFile(resolvedIndexHtml);
        app.use("/api/v*", apiProxyMiddleware);
        app.use((0, compression_1.default)());
        app.use(corsMiddleware);
        app.use(express_1.default.static(resolvedWwwRoot));
        app.get("/*", rateLimiterIndexHtml, middlewareIndexHtml);
        const cockpitPort = this.options.config.cockpitPort;
        const cockpitHost = this.options.config.cockpitHost;
        if (!((_a = this.httpServerCockpit) === null || _a === void 0 ? void 0 : _a.listening)) {
            await new Promise((resolve, reject) => {
                var _a, _b, _c;
                (_a = this.httpServerCockpit) === null || _a === void 0 ? void 0 : _a.once("error", reject);
                (_b = this.httpServerCockpit) === null || _b === void 0 ? void 0 : _b.once("listening", resolve);
                (_c = this.httpServerCockpit) === null || _c === void 0 ? void 0 : _c.listen(cockpitPort, cockpitHost);
            });
        }
        (_b = this.httpServerCockpit) === null || _b === void 0 ? void 0 : _b.on("request", app);
        // the address() method returns a string for unix domain sockets and null
        // if the server is not listening but we don't care about any of those cases
        // so the casting here should be safe. Famous last words... I know.
        const addressInfo = (_c = this.httpServerCockpit) === null || _c === void 0 ? void 0 : _c.address();
        this.log.info(`Cactus Cockpit net.AddressInfo`, addressInfo);
        return addressInfo;
    }
    /**
     * Installs the own endpoints of the API server such as the ones providing
     * healthcheck and monitoring information.
     * @param app
     */
    async getOrCreateWebServices(app) {
        const fnTag = `${this.className}#getOrCreateWebServices()}`;
        const { log } = this;
        const { logLevel } = this.options.config;
        const pluginRegistry = await this.getOrInitPluginRegistry();
        {
            const oasPath = openapi_json_1.default.paths["/api/v1/api-server/get-open-api-spec"];
            const operationId = oasPath.get.operationId;
            const opts = {
                oas: openapi_json_1.default,
                oasPath,
                operationId,
                path: oasPath.get["x-hyperledger-cacti"].http.path,
                pluginRegistry,
                verbLowerCase: oasPath.get["x-hyperledger-cacti"].http.verbLowerCase,
                logLevel,
            };
            const endpoint = new get_open_api_spec_v1_endpoint_1.GetOpenApiSpecV1Endpoint(opts);
            await (0, cactus_core_1.registerWebServiceEndpoint)(app, endpoint);
        }
        const healthcheckHandler = (req, res) => {
            res.json({
                success: true,
                createdAt: new Date(),
                memoryUsage: process.memoryUsage(),
            });
        };
        const { "/api/v1/api-server/healthcheck": oasPath } = openapi_json_1.default.paths;
        const { http } = oasPath.get["x-hyperledger-cacti"];
        const { path: httpPath, verbLowerCase: httpVerb } = http;
        if (!(0, cactus_common_1.isExpressHttpVerbMethodName)(httpVerb)) {
            const eMsg = `${fnTag} Invalid HTTP verb "${httpVerb}" in cmd-api-server OpenAPI specification for HTTP path: "${httpPath}"`;
            throw new run_time_error_cjs_1.RuntimeError(eMsg);
        }
        app[httpVerb](httpPath, healthcheckHandler);
        this.wsApi.on("connection", (socket) => {
            const { id } = socket;
            const transport = socket.conn.transport.name; // in most cases, "polling"
            log.debug(`Socket connected. ID=${id} transport=%o`, transport);
            socket.conn.on("upgrade", () => {
                const upgradedTransport = socket.conn.transport.name; // in most cases, "websocket"
                log.debug(`Socket upgraded ID=${id} transport=%o`, upgradedTransport);
            });
            socket.on(typescript_axios_1.WatchHealthcheckV1.Subscribe, () => new watch_healthcheck_v1_endpoint_1.WatchHealthcheckV1Endpoint({
                process,
                socket,
                logLevel,
            }).subscribe());
        });
        const prometheusExporterHandler = (req, res) => {
            this.getPrometheusExporterMetrics().then((resBody) => {
                res.status(200);
                res.send(resBody);
            });
        };
        const { "/api/v1/api-server/get-prometheus-exporter-metrics": oasPathPrometheus, } = openapi_json_1.default.paths;
        const { http: httpPrometheus } = oasPathPrometheus.get["x-hyperledger-cacti"];
        const { path: httpPathPrometheus, verbLowerCase: httpVerbPrometheus } = httpPrometheus;
        if (!(0, cactus_common_1.isExpressHttpVerbMethodName)(httpVerbPrometheus)) {
            const eMsg = `${fnTag} Invalid HTTP verb "${httpVerbPrometheus}" in cmd-api-server OpenAPI specification for HTTP path: "${httpPathPrometheus}"`;
            throw new run_time_error_cjs_1.RuntimeError(eMsg);
        }
        app[httpVerbPrometheus](httpPathPrometheus, prometheusExporterHandler);
    }
    async createCrpcExpressMiddlewareHandler() {
        const { crpcRoutesHandler, svcCount } = await this.createCrpcRoutesHandler();
        const crpcMiddlewareHandler = (0, connect_express_1.expressConnectMiddleware)({
            routes: crpcRoutesHandler,
        });
        return { svcCount, crpcMiddlewareHandler };
    }
    async createCrpcRoutesHandler() {
        const fnTag = `${this.className}#registerCrpcServices()}`;
        const { log } = this;
        const crpcSvcRegistrations = await this.createCrpcServicesOfPlugins();
        const crpcSvcRegCount = crpcSvcRegistrations.length;
        log.debug("%s Obtained %o Crpc registrations.", fnTag, crpcSvcRegCount);
        const crpcRoutesHandler = (router) => {
            log.debug("%s expressConnectMiddleware() routes handler", fnTag);
            crpcSvcRegistrations.forEach((it) => {
                log.debug("%s Registering %s", fnTag, it.serviceName);
                router.service(it.definition, it.implementation, it.options);
            });
        };
        return { svcCount: crpcSvcRegCount, crpcRoutesHandler };
    }
    async startCrpcServer() {
        const fn = `${this.className}#startCrpcServer()`;
        const { log, options } = this;
        const { config } = options;
        const { crpcHost, crpcPort } = config;
        const { crpcRoutesHandler, svcCount } = await this.createCrpcRoutesHandler();
        log.debug("%s Registering %o CRPC routes handler(s).", fn, svcCount);
        await this.crpcServer.register(connect_fastify_1.fastifyConnectPlugin, {
            routes: crpcRoutesHandler,
            shutdownTimeoutMs: 5000,
            grpc: true,
            grpcWeb: true,
        });
        log.debug("%s Fastify CRPC service registration OK", fn);
        const crpcUrl = await this.crpcServer.listen({
            host: crpcHost,
            port: crpcPort,
        });
        log.debug("%s Fastify listen() crpcUrl: %s", fn, crpcUrl);
        const [addressInfoCrpc, ...addresses] = this.crpcServer.addresses();
        log.debug("%s server is listening at", fn, addressInfoCrpc, addresses);
        return { crpcUrl, addressInfoCrpc };
    }
    async startGrpcServer() {
        const fnTag = `${this.className}#startGrpcServer()`;
        const { log } = this;
        const { logLevel } = this.options.config;
        const pluginRegistry = await this.getOrInitPluginRegistry();
        return new Promise((resolve, reject) => {
            // const grpcHost = "0.0.0.0"; // FIXME - make this configurable (config-service.ts)
            const grpcHost = "127.0.0.1"; // FIXME - make this configurable (config-service.ts)
            const grpcHostAndPort = `${grpcHost}:${this.options.config.grpcPort}`;
            const grpcTlsCredentials = this.options.config.grpcMtlsEnabled
                ? grpc_js_2.ServerCredentials.createSsl(Buffer.from(this.options.config.apiTlsCertPem), [
                    {
                        cert_chain: Buffer.from(this.options.config.apiTlsCertPem),
                        private_key: Buffer.from(this.options.config.apiTlsKeyPem),
                    },
                ], true)
                : grpc_js_2.ServerCredentials.createInsecure();
            this.grpcServer.addService(default_service.org.hyperledger.cactus.cmd_api_server
                .DefaultServiceClient.service, new grpc_server_api_server_1.GrpcServerApiServer());
            log.debug("Installing gRPC services of IPluginGrpcService instances...");
            pluginRegistry.getPlugins().forEach(async (x) => {
                if (!(0, cactus_core_api_1.isIPluginGrpcService)(x)) {
                    this.log.debug("%s skipping %s instance", fnTag, x.getPackageName());
                    return;
                }
                const opts = { logLevel };
                log.info("%s Creating gRPC service of: %s", fnTag, x.getPackageName());
                const svcPairs = await x.createGrpcSvcDefAndImplPairs(opts);
                log.debug("%s Obtained %o gRPC svc pairs OK", fnTag, svcPairs.length);
                svcPairs.forEach(({ definition, implementation }) => {
                    const svcNames = Object.values(definition).map((x) => x.originalName);
                    const svcPaths = Object.values(definition).map((x) => x.path);
                    log.debug("%s Adding gRPC svc names %o ...", fnTag, svcNames);
                    log.debug("%s Adding gRPC svc paths %o ...", fnTag, svcPaths);
                    this.grpcServer.addService(definition, implementation);
                    log.debug("%s Added gRPC svc OK ...", fnTag);
                });
                log.info("%s Added gRPC service of: %s OK", fnTag, x.getPackageName());
            });
            log.debug("%s Installed all IPluginGrpcService instances OK", fnTag);
            this.grpcServer.bindAsync(grpcHostAndPort, grpcTlsCredentials, (error, port) => {
                if (error) {
                    this.log.error("%s Binding gRPC failed: ", fnTag, error);
                    return reject(new run_time_error_cjs_1.RuntimeError(fnTag + " gRPC bindAsync:", error));
                }
                else {
                    this.log.info("%s gRPC server bound to port %o OK", fnTag, port);
                }
                const portStr = port.toString(10);
                this.boundGrpcHostPort = grpcHost.concat(":").concat(portStr);
                log.info("%s boundGrpcHostPort=%s", fnTag, this.boundGrpcHostPort);
                const family = (0, determine_address_family_1.determineAddressFamily)(grpcHost);
                resolve({ address: grpcHost, port, family });
            });
        });
    }
    async createCrpcServicesOfPlugins() {
        const fnTag = `${this.className}#startCrpcServer()`;
        const { log } = this;
        const { logLevel } = this.options.config;
        const pluginRegistry = await this.getOrInitPluginRegistry();
        log.debug("Installing crpc services of IPluginCrpcService instances...");
        const out = [];
        const plugins = pluginRegistry.getPlugins();
        const tasksDone = plugins.map(async (x) => {
            if (!(0, cactus_core_api_1.isIPluginCrpcService)(x)) {
                this.log.debug("%s skipping %s instance", fnTag, x.getPackageName());
                return;
            }
            const opts = { logLevel };
            log.info("%s Creating crpc service of: %s", fnTag, x.getPackageName());
            const svcRegistrations = await x.createCrpcSvcRegistrations(opts);
            log.debug("%s Got %o Crpc svc defs:", fnTag, svcRegistrations.length);
            svcRegistrations.forEach((it) => out.push(it));
        });
        await Promise.all(tasksDone);
        return out;
    }
    async startApiServer() {
        const { options, expressApi: app, wsApi } = this;
        const { config } = options;
        const { authorizationConfigJson: authzConf, authorizationProtocol: authzProtocol, logLevel, } = config;
        const apiServerOptions = config;
        const pluginRegistry = await this.getOrInitPluginRegistry();
        const { svcCount, crpcMiddlewareHandler } = await this.createCrpcExpressMiddlewareHandler();
        this.log.info("Registered %o Crpc services OK", svcCount);
        app.use(crpcMiddlewareHandler);
        app.use((0, compression_1.default)());
        const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv;
        const allowedDomains = apiCorsDomainCsv.split(",");
        const corsMiddleware = this.createCorsMiddleware(allowedDomains);
        app.use(corsMiddleware);
        app.use(body_parser_1.default.json({ limit: "50mb" }));
        // Add custom replacer to handle bigint responses correctly
        app.set("json replacer", this.stringifyBigIntReplacer);
        const authzFactoryOptions = { apiServerOptions, pluginRegistry, logLevel };
        const authzFactory = new authorizer_factory_1.AuthorizerFactory(authzFactoryOptions);
        await authzFactory.initOnce();
        const authorizerO = await authzFactory.createMiddleware(authzProtocol, authzConf);
        if (authorizerO.isPresent()) {
            const authorizer = authorizerO.get();
            this.checkNonExemptUnprotectedEps(authzFactory);
            app.use(authorizer);
            this.log.info(`Authorization request handler configured OK.`);
        }
        this.getOrCreateWebServices(app); // The API server's own endpoints
        this.log.info(`Starting to install web services...`);
        const webServicesInstalled = pluginRegistry
            .getPlugins()
            .filter((pluginInstance) => (0, cactus_core_api_1.isIPluginWebService)(pluginInstance))
            .map(async (plugin) => {
            const p = plugin;
            await p.getOrCreateWebServices();
            const apiSpec = p.getOpenApiSpec();
            if (apiSpec)
                await (0, cactus_core_2.installOpenapiValidationMiddleware)({ app, apiSpec, logLevel });
            const webSvcs = await p.registerWebServices(app, wsApi);
            return webSvcs;
        });
        const endpoints2D = await Promise.all(webServicesInstalled);
        this.log.info(`Installed ${webServicesInstalled.length} web service(s) OK`);
        const endpoints = endpoints2D.reduce((acc, val) => acc.concat(val), []);
        endpoints.forEach((ep) => this.log.info(`Endpoint={path=${ep.getPath()}}`));
        const apiPort = this.options.config.apiPort;
        const apiHost = this.options.config.apiHost;
        if (!this.httpServerApi.listening) {
            await new Promise((resolve, reject) => {
                this.httpServerApi.once("error", reject);
                this.httpServerApi.once("listening", resolve);
                this.httpServerApi.listen(apiPort, apiHost);
            });
        }
        this.httpServerApi.on("request", app);
        // the address() method returns a string for unix domain sockets and null
        // if the server is not listening but we don't care about any of those cases
        // so the casting here should be safe. Famous last words... I know.
        const addressInfo = this.httpServerApi.address();
        this.log.info(`Cactus API net.AddressInfo`, addressInfo);
        const wsOptions = Object.assign({ path: cactus_core_api_1.Constants.SocketIoConnectionPathV1, serveClient: false }, this.options.wsOptions);
        this.wsApi.attach(this.httpServerApi, wsOptions);
        if (authorizerO.isPresent()) {
            const socketIoAuthorizer = (0, socketio_jwt_1.authorize)(Object.assign(Object.assign({}, authzConf.socketIoJwtOptions), { onAuthentication: (decodedToken) => {
                    this.log.debug("Socket authorized OK: %o", decodedToken);
                } }));
            this.wsApi.use(socketIoAuthorizer);
        }
        return addressInfo;
    }
    checkNonExemptUnprotectedEps(factory) {
        const { config } = this.options;
        const { authorizationConfigJson } = config;
        const { unprotectedEndpointExemptions } = authorizationConfigJson;
        const nonExempts = factory.unprotectedEndpoints.filter((nse) => !unprotectedEndpointExemptions.some((pt) => nse.getPath().match(pt)));
        if (nonExempts.length > 0) {
            const csv = nonExempts.map((ep) => ep.getPath()).join(", ");
            const { E_NON_EXEMPT_UNPROTECTED_ENDPOINTS } = ApiServer;
            throw new Error(`${E_NON_EXEMPT_UNPROTECTED_ENDPOINTS} ${csv}`);
        }
    }
    createCorsMiddleware(allowedDomains) {
        const allDomainsOk = allowedDomains.includes("*");
        const corsOptionsDelegate = (req, callback) => {
            const origin = req.headers["origin"];
            const isDomainOk = origin && allowedDomains.includes(origin);
            // this.log.debug("CORS %j %j %s", allDomainsOk, isDomainOk, req.originalUrl);
            let corsOptions;
            if (allDomainsOk) {
                corsOptions = { origin: "*" }; // reflect (enable) the all origins in the CORS response
            }
            else if (isDomainOk) {
                corsOptions = { origin }; // reflect (enable) the requested origin in the CORS response
            }
            else {
                corsOptions = { origin: false }; // disable CORS for this request
            }
            callback(null, corsOptions); // callback expects two parameters: error and options
        };
        return (0, cors_1.default)(corsOptionsDelegate);
    }
    /**
     * `JSON.stringify` replacer function to handle BigInt.
     * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json
     */
    stringifyBigIntReplacer(_key, value) {
        if (typeof value === "bigint") {
            return value.toString();
        }
        return value;
    }
}
exports.ApiServer = ApiServer;
ApiServer.CLASS_NAME = "ApiServer";
ApiServer.E_POST_CRASH_SHUTDOWN = "API server failed to shut itself down, will ignore this because we were already crashing anyway...";
ApiServer.E_NON_EXEMPT_UNPROTECTED_ENDPOINTS = `Non-exempt unprotected endpoints found. ` +
    `You can allow them as unprotected via the configuration of the ` +
    `API server by specifying an array of patterns in the property ` +
    `"unprotectedEndpointExemptions" of "authorizationConfigJson". ` +
    `This mechanism is meant to foster DevOps where both dev & ops ` +
    `work together in making secure application deployments a reality. ` +
    `The comma separated list of unprotected endpoints that were not marked as exempt: `;
//# sourceMappingURL=data:application/json;base64, 