"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.expose = exports.start = exports.getTLSOptions = void 0;
const fastify_1 = __importDefault(require("fastify"));
const path_1 = require("path");
const redlock_1 = require("redlock");
const adapter_1 = require("./adapter");
const background_executor_1 = require("./background-executor");
const router_1 = __importDefault(require("./debug/router"));
const metrics_1 = require("./metrics");
const util_1 = require("./util");
const validation_1 = require("./validation");
const logger = (0, util_1.makeLogger)('Main');
const VERSION = process.env['npm_package_version'];
const getTLSOptions = (adapterSettings) => {
    if (!adapterSettings.TLS_ENABLED && !adapterSettings.MTLS_ENABLED) {
        return {};
    }
    if (adapterSettings.TLS_ENABLED && adapterSettings.MTLS_ENABLED) {
        throw new Error('TLS_ENABLED and MTLS_ENABLED cannot both be set to true.');
    }
    if (!adapterSettings.TLS_PRIVATE_KEY ||
        !adapterSettings.TLS_PUBLIC_KEY ||
        !adapterSettings.TLS_CA) {
        const TLSOption = adapterSettings.TLS_ENABLED ? 'TLS_ENABLED' : 'MTLS_ENABLED';
        throw new Error(`TLS_PRIVATE_KEY, TLS_PUBLIC_KEY, and TLS_CA environment variables are required when ${TLSOption} is set to true.`);
    }
    const httpsOptions = {
        key: adapterSettings.TLS_PRIVATE_KEY,
        cert: adapterSettings.TLS_PUBLIC_KEY,
        ca: adapterSettings.TLS_CA,
        passphrase: adapterSettings.TLS_PASSPHRASE,
        requestCert: adapterSettings.MTLS_ENABLED,
    };
    return { https: httpsOptions };
};
exports.getTLSOptions = getTLSOptions;
/**
 * Main function for the framework.
 * Initializes config and dependencies, uses those to initialize Transports, and starts listening for requests.
 *
 * @param adapter - an object describing an External Adapter
 * @param dependencies - an optional object with adapter dependencies to inject
 * @returns a Promise that resolves to the http.Server listening for connections
 */
const start = async (adapter, dependencies) => {
    if (!(adapter instanceof adapter_1.Adapter)) {
        throw new Error('The adapter has not been initialized as an instance of the Adapter class, exiting.');
    }
    // Initialize adapter (create dependencies, inject them, build endpoint map, etc.)
    await adapter.initialize(dependencies);
    let api = undefined;
    let metricsApi = undefined;
    if (adapter.config.settings.METRICS_ENABLED &&
        adapter.config.settings.EXPERIMENTAL_METRICS_ENABLED) {
        metricsApi = (0, metrics_1.setupMetricsServer)(adapter.name, adapter.config.settings);
    }
    // Optional Promise to indicate that the API is shutting down (for us to close background executors)
    let apiShutdownPromise;
    if (adapter.config.settings.EA_MODE === 'reader' ||
        adapter.config.settings.EA_MODE === 'reader-writer') {
        // Main REST API server to handle incoming requests
        api = await buildRestApi(adapter);
        // Add a hook on close to use on the background execution loop to stop it
        apiShutdownPromise = new Promise((resolve) => {
            api?.addHook('onClose', async () => {
                // Used in the RedisCache lock method for testing purposes
                adapter.shutdownNotifier.emit('onClose');
                resolve();
            });
        });
    }
    else {
        logger.info('REST API is disabled; this instance will not process incoming requests.');
    }
    // Listener for unhandled promise rejections that are bubbling up to the top
    // The tests will add many listeners since multiple adapters are started in the same process
    if (process.env['NODE_ENV'] === 'test') {
        process.setMaxListeners(0);
    }
    process.on('unhandledRejection', (err) => {
        // Throw redlock execution error for testing purposes
        if (err instanceof redlock_1.ExecutionError) {
            throw err;
        }
        (0, util_1.censorLogs)(() => logger.error({
            name: err.name,
            stack: err.stack,
            message: err.message,
        }));
    });
    if (adapter.config.settings.EA_MODE === 'writer' ||
        adapter.config.settings.EA_MODE === 'reader-writer') {
        // Start background loop that will take care of calling any async Transports
        logger.info('Starting background execution loop');
        (0, background_executor_1.callBackgroundExecutes)(adapter, apiShutdownPromise);
    }
    else {
        logger.info('Background executor is disabled; this instance will not perform async background executes.');
    }
    return { api, metricsApi };
};
exports.start = start;
const expose = async (adapter, dependencies) => {
    const { api, metricsApi } = await (0, exports.start)(adapter, dependencies);
    const exposeApp = async (app, port) => {
        if (app) {
            try {
                await app.listen({ port, host: adapter.config.settings.EA_HOST });
            }
            catch (err) {
                (0, util_1.censorLogs)(() => logger.fatal(`There was an error when starting the server: ${err}`));
                process.exit();
            }
            logger.info(`Listening on port ${app.server.address().port}`);
        }
    };
    // Start listening for incoming requests
    await exposeApp(api, adapter.config.settings.EA_PORT);
    await exposeApp(metricsApi, adapter.config.settings.METRICS_PORT);
    // Make sure the cache lock has been acquired before returning
    if (adapter.lockAcquiredPromise) {
        try {
            await adapter.lockAcquiredPromise;
        }
        catch (error) {
            await api?.close();
            await metricsApi?.close();
            throw error;
        }
    }
    // We return only the main API to maintain backwards compatibility
    return api;
};
exports.expose = expose;
async function buildRestApi(adapter) {
    const TLSOptions = (0, exports.getTLSOptions)(adapter.config.settings);
    const app = (0, fastify_1.default)({
        ...TLSOptions,
        bodyLimit: adapter.config.settings.MAX_PAYLOAD_SIZE_LIMIT,
    });
    // Add healthcheck endpoint before middlewares to bypass them
    app.get((0, path_1.join)(adapter.config.settings.BASE_URL, 'health'), (req, res) => {
        res.status(200).send({ message: 'OK', version: VERSION });
    });
    // If specified, serve debug endpoints to assist in issue triaging
    if (adapter.config.settings.DEBUG_ENDPOINTS === true) {
        (0, router_1.default)(app, adapter);
        logger.info('Serving debug endpoints');
    }
    // Use global error handling
    app.setErrorHandler(validation_1.errorCatchingMiddleware);
    // Always reply with json content
    app.addHook('preHandler', (_, reply, done) => {
        reply.headers({ 'content-type': 'application/json; charset=utf-8' });
        done();
    });
    app.register(async (router) => {
        // Set up "middlewares" (hooks in fastify)
        router.addHook('preHandler', (0, validation_1.validatorMiddleware)(adapter));
        if (adapter.config.settings.CORRELATION_ID_ENABLED) {
            router.addHook('onRequest', util_1.loggingContextMiddleware);
        }
        router.route({
            url: adapter.config.settings.BASE_URL,
            method: 'POST',
            handler: async (req, reply) => {
                const response = await adapter.handleRequestWithValidation(req, reply);
                return reply.code(response.statusCode || 200).send(response);
            },
        });
        if (adapter.config.settings.METRICS_ENABLED &&
            adapter.config.settings.EXPERIMENTAL_METRICS_ENABLED) {
            router.addHook('onResponse', metrics_1.buildMetricsMiddleware);
        }
    });
    return app;
}
//# sourceMappingURL=index.js.map