"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createExpressApp = exports.createApiError = void 0;
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const stream_1 = __importDefault(require("stream"));
const util_1 = __importDefault(require("util"));
const express_1 = __importDefault(require("express"));
const body_parser_1 = __importDefault(require("body-parser"));
const node_fetch_1 = __importDefault(require("node-fetch"));
const safe_compare_1 = __importDefault(require("safe-compare"));
const createApiError = ({ message, extraInfo, status = 500, }) => {
    const error = new Error(message);
    error.status = status;
    error.extraInfo = extraInfo;
    return error;
};
exports.createApiError = createApiError;
const createExpressApp = ({ _console = console, _fetch = node_fetch_1.default, _process = process, _unlinkFile = fs_1.default.promises.unlink, apiKeyEnvVarName = 'LAMBDA_API_KEY', requiredApiKeyParam = 'api_key', requiredDownloadUrlParam = 'download_url', tmpDir = os_1.default.tmpdir(), xpiFilename = 'input.xpi', } = {}) => (handler) => {
    const app = (0, express_1.default)();
    const allowedOrigin = _process.env.ALLOWED_ORIGIN || null;
    if (!allowedOrigin) {
        throw new Error('ALLOWED_ORIGIN is not set or unexpectedly empty!');
    }
    const apiKey = _process.env[apiKeyEnvVarName] || null;
    if (apiKey) {
        // Delete the env var to not expose it to add-ons.
        // eslint-disable-next-line no-param-reassign
        delete _process.env[apiKeyEnvVarName];
    }
    // Parse JSON body requests.
    app.use(body_parser_1.default.json());
    // This middleware handles the common logic needed to expose our tools. It
    // adds a new `xpiFilepath` attribute to the Express request or returns an
    // error that will be converted to an API error by the error handler
    // middleware declared at the bottom of the middleware chain.
    app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
        const allowedMethods = ['POST'];
        if (req.headers['content-type'] !== 'application/json') {
            // We do not throw because we are inside a callback, so we pass an error
            // to the next middleware, which will be the error handler.
            // See: https://expressjs.com/en/guide/error-handling.html
            next((0, exports.createApiError)({
                message: 'unsupported content type',
                status: 415,
            }));
            return;
        }
        if (typeof req.body[requiredApiKeyParam] === 'undefined') {
            next((0, exports.createApiError)({
                message: `missing "${requiredApiKeyParam}" parameter`,
                status: 400,
            }));
            return;
        }
        if (!apiKey || !(0, safe_compare_1.default)(apiKey, req.body[requiredApiKeyParam])) {
            next((0, exports.createApiError)({
                message: 'authentication has failed',
                status: 401,
            }));
            return;
        }
        if (!allowedMethods
            .map((method) => method.toLowerCase())
            .includes(req.method.toLowerCase())) {
            next((0, exports.createApiError)({ message: 'method not allowed', status: 405 }));
            return;
        }
        const downloadURL = req.body[requiredDownloadUrlParam];
        if (!downloadURL) {
            next((0, exports.createApiError)({
                message: `missing "${requiredDownloadUrlParam}" parameter`,
                status: 400,
            }));
            return;
        }
        if (!downloadURL.startsWith(allowedOrigin)) {
            next((0, exports.createApiError)({ message: 'invalid origin', status: 400 }));
            return;
        }
        try {
            const xpiFilepath = path_1.default.join(tmpDir, xpiFilename);
            const streamPipeline = util_1.default.promisify(stream_1.default.pipeline);
            const response = yield _fetch(downloadURL);
            if (!response.ok) {
                throw new Error(`unexpected response ${response.statusText}`);
            }
            yield streamPipeline(response.body, fs_1.default.createWriteStream(xpiFilepath));
            req.xpiFilepath = xpiFilepath;
            // Add a listener that will run code after the response is sent.
            res.on('finish', () => {
                _unlinkFile(xpiFilepath).catch((error) => {
                    _console.error(`_unlinkFile(): ${error}`);
                });
            });
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
        }
        catch (err) {
            next((0, exports.createApiError)({
                message: 'failed to download file',
                extraInfo: err.message,
            }));
            return;
        }
        next();
    }));
    // We register the handler for the tool that will be exposed. This handler is
    // guaranteed to have a valid `xpiFilepath` stored on disk.
    app.post('/', handler);
    // NotFound handler.
    app.use((req, res, next) => {
        next((0, exports.createApiError)({ message: 'not found', status: 404 }));
    });
    // Error handler. Even though we are not using `next`, it must be kept
    // because the Express error handler signature requires 4 arguments.
    app.use(
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    (err, req, res, next) => {
        const error = {
            error: err.message,
            extra_info: err.extraInfo || null,
        };
        res.status(err.status || 500).json(error);
        // Also send the error to the cloud provider.
        _console.error(error);
    });
    return app;
};
exports.createExpressApp = createExpressApp;
