"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fixRawPath = exports.encodePath = exports.sanitizeHeaders = exports.invokeActionFromEvent = exports.invokeSourceFromEvent = exports.requestIdFromEvent = exports.urlFromEvent = exports.cookiesFromEvent = exports.headerValueFromEvent = exports.singleValueHeadersFromEvent = exports.searchFromEvent = exports.pathFromEvent = exports.methodFromEvent = exports.isBufferProxyEvent = exports.isApiGatewayV2Event = exports.isConsoleEvent = void 0;
const constants_1 = require("../constants");
const querystring_1 = __importDefault(require("querystring"));
function isConsoleEvent(event) {
    return 'action' in event;
}
exports.isConsoleEvent = isConsoleEvent;
function isApiGatewayV2Event(event) {
    return 'version' in event && event.version === '2.0';
}
exports.isApiGatewayV2Event = isApiGatewayV2Event;
function isBufferProxyEvent(event) {
    return 'multiValueHeaders' in event;
}
exports.isBufferProxyEvent = isBufferProxyEvent;
function methodFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        return event.requestContext.http.method;
    }
    else {
        return event.httpMethod;
    }
}
exports.methodFromEvent = methodFromEvent;
function pathFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        return encodePath(fixRawPath(event.rawPath));
    }
    else {
        return event.path ? encodePath(event.path) : undefined;
    }
}
exports.pathFromEvent = pathFromEvent;
function searchFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        // We parse query string here, to decode encoded characters
        // and then encode them again to get normalized query string.
        // This is necessary, otherwise node would throw an error if special characters are not encoded
        // and we also don't want to encode it twice.
        // NOTE: SearchParams.toString() encodes spaces as '+' instead of '%20'.
        // Both ways are valid but with XBP spaces are encoded as '%20', so we want to keep it consistent.
        if (!event.rawQueryString)
            return '';
        const parsedQueryString = querystring_1.default.parse(event.rawQueryString);
        return `?${querystring_1.default.stringify(parsedQueryString)}`;
    }
    else if (isBufferProxyEvent(event)) {
        return normalizeLambdaQuery(event);
    }
    else {
        return undefined;
    }
}
exports.searchFromEvent = searchFromEvent;
function singleValueHeadersFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        return event.headers;
    }
    else if (isBufferProxyEvent(event)) {
        return Object.fromEntries(Object.entries(event.multiValueHeaders).map(([key, value]) => [key, value[0]]));
    }
    else {
        return {};
    }
}
exports.singleValueHeadersFromEvent = singleValueHeadersFromEvent;
function headerValueFromEvent(event, headerName) {
    const headers = singleValueHeadersFromEvent(event);
    return headers[headerName] || headers[headerName.toLowerCase()];
}
exports.headerValueFromEvent = headerValueFromEvent;
function cookiesFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        return event.cookies || [];
    }
    else if (isBufferProxyEvent(event)) {
        return event.multiValueHeaders.cookie || [];
    }
    else {
        return [];
    }
}
exports.cookiesFromEvent = cookiesFromEvent;
function urlFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        return fixRawPath(event.rawPath) + (event.rawQueryString ? `?${event.rawQueryString}` : '');
    }
    else if (isBufferProxyEvent(event)) {
        return event.rawUrl || event.path + normalizeLambdaQuery(event);
    }
    else {
        return undefined;
    }
}
exports.urlFromEvent = urlFromEvent;
function requestIdFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        return event.headers[constants_1.HTTP_HEADERS.xRequestId] || event.headers[constants_1.HTTP_HEADERS.xEcUUID];
    }
    else if (isBufferProxyEvent(event)) {
        return (flattenArray(event.multiValueHeaders[constants_1.HTTP_HEADERS.xRequestId]) ||
            flattenArray(event.multiValueHeaders[constants_1.HTTP_HEADERS.xEcUUID]));
    }
    else {
        return undefined;
    }
}
exports.requestIdFromEvent = requestIdFromEvent;
function invokeSourceFromEvent(event) {
    if (isApiGatewayV2Event(event)) {
        return constants_1.INVOKE_SOURCES.functionUrl;
    }
    else if (isBufferProxyEvent(event)) {
        return constants_1.INVOKE_SOURCES.bufferProxy;
    }
    else if (isConsoleEvent(event)) {
        return constants_1.INVOKE_SOURCES.console;
    }
    throw new Error('Unknown event type');
}
exports.invokeSourceFromEvent = invokeSourceFromEvent;
function invokeActionFromEvent(event) {
    // When x-cloud-functions-hint header is present, we want to run only the serverless code
    // and skip the simulator (behind the Edge).
    // When we run the function locally, or when we invoke function directly through the Function URL,
    // we want to run the simulator. This is also useful for testing and debugging.
    if (isConsoleEvent(event)) {
        return event.action;
    }
    if (isApiGatewayV2Event(event)) {
        const cloudFunctionsHintHeader = event.headers[constants_1.HTTP_HEADERS.xCloudFunctionsHint];
        return cloudFunctionsHintHeader ? constants_1.INVOKE_ACTIONS.serverless : constants_1.INVOKE_ACTIONS.simulator;
    }
    if (isBufferProxyEvent(event)) {
        const cloudFunctionsHintHeader = event.multiValueHeaders[constants_1.HTTP_HEADERS.xCloudFunctionsHint];
        return cloudFunctionsHintHeader ? constants_1.INVOKE_ACTIONS.serverless : constants_1.INVOKE_ACTIONS.simulator;
    }
    throw new Error('Unknown event type');
}
exports.invokeActionFromEvent = invokeActionFromEvent;
function flattenArray(arr) {
    return Array.isArray(arr) ? arr.join('') : arr;
}
/**
 * Normalizes query to not have arrays if only one value of certain key exists
 */
function normalizeLambdaQuery(event) {
    let query;
    if (event.multiValueQueryStringParameters &&
        Object.keys(event.multiValueQueryStringParameters).length) {
        Object.keys(event.multiValueQueryStringParameters).forEach(key => {
            // keep backwards compatibility so that arrays with a single value are
            // converted to just be that value:
            const curVal = event.multiValueQueryStringParameters[key];
            if (Array.isArray(curVal) && curVal.length === 1) {
                event.multiValueQueryStringParameters[key] = curVal[0];
            }
        });
        // Querystring.stringify serializes arrays as repeating keys
        // by default: so we get correct 'a=b&a=c&a=d' instead of 'a[0]=b&a[1]=c&a[2]=d' format
        // for input like { a: ['b', 'c', 'd'] }
        query = querystring_1.default.stringify(event.multiValueQueryStringParameters);
    }
    return query ? '?' + query : '';
}
/**
 * Removes restricted headers and values from the headers object.
 * For example, all internal "x-amzn-*" headers are removed.
 * These headers are added by AWS into both Function URLs req and res.
 * If these headers are returned in the response,
 * for example because app proxies traffic to itself or another Edgio site,
 * AWS returns 500 status code with InternalFailure description.
 * @param headers Headers object
 */
function sanitizeHeaders(headers) {
    // Make shallow copy of headers object
    const sanitizedHeaders = { ...headers };
    Object.keys(sanitizedHeaders).forEach(key => {
        // Remove all AWS internal headers to prevent 500 errors
        if (key.startsWith('x-amzn-')) {
            delete sanitizedHeaders[key];
        }
    });
    return sanitizedHeaders;
}
exports.sanitizeHeaders = sanitizeHeaders;
/**
 * Encodes just characters that are not valid in a URL path and
 * preserves all already encoded characters (to avoid double encoding).
 * @param path Path to encode
 * @returns Encoded path
 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
 * @example encodePath('/path with spaces') => '/path%20with%20spaces' // encodes unencoded spaces
 * @example encodePath('/path%20with%20spaces') => '/path%20with%20spaces' // preserves encoded spaces
 * @example encodePath('/path%2B') => '/path%2B' // preserves encoded '+'
 * @example encodePath('/path+') => '/path+' // preserves unencoded '+'
 */
function encodePath(path) {
    return path.replace(/[^A-Za-z0-9\-/.*()+$!~'@,;:&=_]/g, char => {
        // Preserve already encoded character
        if (char === '%')
            return char;
        // Encode the character
        return encodeURIComponent(char);
    });
}
exports.encodePath = encodePath;
/**
 * Fixes path sent by API Gateway v2.
 * When browser sends request path with '+', Function URL decodes it and gives us ' '.
 * This is not valid behavior. Spaces can be encoded as '+' only in query strings
 * with application/x-www-form-urlencoded but not in paths. In paths, spaces encoded only as '%20'.
 * When browser sends request path with ' ', or '%20', Function URL always decodes it to '%20'.
 * This is valid behavior.
 * We need to correct it here.
 * NOTE: It's a regional bug. I was able to reproduce it only with Function URL in us-east-1 region.
 * The us-east-2 region worked. But it is not just about the region, with dev aws account,
 * it worked in us-east-1 region too.
 */
function fixRawPath(rawPath) {
    // Replace ' ' with '+', because we know it was originally '+'
    // and the actual space would be '%20'.
    return rawPath.replace(/ /g, '+');
}
exports.fixRawPath = fixRawPath;
