"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const content_type_1 = require("next/dist/compiled/content-type");
const raw_body_1 = __importDefault(require("next/dist/compiled/raw-body"));
const stream_1 = require("stream");
const utils_1 = require("../lib/utils");
const crypto_utils_1 = require("./crypto-utils");
const load_components_1 = require("./load-components");
async function apiResolver(req, res, params, resolverModule, apiContext, onError) {
    var _a;
    const apiReq = req;
    const apiRes = res;
    try {
        if (!resolverModule) {
            res.statusCode = 404;
            res.end('Not Found');
            return;
        }
        const config = resolverModule.config || {};
        const bodyParser = ((_a = config.api) === null || _a === void 0 ? void 0 : _a.bodyParser) !== false;
        // Parsing of cookies
        setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req));
        // Parsing query string
        setLazyProp({ req: apiReq, params }, 'query', getQueryParser(req));
        // // Parsing of body
        if (bodyParser) {
            apiReq.body = await parseBody(apiReq, config.api && config.api.bodyParser && config.api.bodyParser.sizeLimit
                ? config.api.bodyParser.sizeLimit
                : '1mb');
        }
        apiRes.status = statusCode => sendStatusCode(apiRes, statusCode);
        apiRes.send = data => sendData(apiRes, data);
        apiRes.json = data => sendJson(apiRes, data);
        apiRes.setPreviewData = (data, options = {}) => setPreviewData(apiRes, data, Object.assign({}, apiContext, options));
        apiRes.clearPreviewData = () => clearPreviewData(apiRes);
        const resolver = load_components_1.interopDefault(resolverModule);
        let wasPiped = false;
        if (process.env.NODE_ENV !== 'production') {
            // listen for pipe event and don't show resolve warning
            res.once('pipe', () => (wasPiped = true));
        }
        // Call API route method
        await resolver(req, res);
        if (process.env.NODE_ENV !== 'production' && !utils_1.isResSent(res) && !wasPiped) {
            console.warn(`API resolved without sending a response for ${req.url}, this may result in stalled requests.`);
        }
    }
    catch (err) {
        if (err instanceof ApiError) {
            sendError(apiRes, err.statusCode, err.message);
        }
        else {
            console.error(err);
            if (onError)
                await onError({ err });
            sendError(apiRes, 500, 'Internal Server Error');
        }
    }
}
exports.apiResolver = apiResolver;
/**
 * Parse incoming message like `json` or `urlencoded`
 * @param req request object
 */
async function parseBody(req, limit) {
    const contentType = content_type_1.parse(req.headers['content-type'] || 'text/plain');
    const { type, parameters } = contentType;
    const encoding = parameters.charset || 'utf-8';
    let buffer;
    try {
        buffer = await raw_body_1.default(req, { encoding, limit });
    }
    catch (e) {
        if (e.type === 'entity.too.large') {
            throw new ApiError(413, `Body exceeded ${limit} limit`);
        }
        else {
            throw new ApiError(400, 'Invalid body');
        }
    }
    const body = buffer.toString();
    if (type === 'application/json' || type === 'application/ld+json') {
        return parseJson(body);
    }
    else if (type === 'application/x-www-form-urlencoded') {
        const qs = require('querystring');
        return qs.decode(body);
    }
    else {
        return body;
    }
}
exports.parseBody = parseBody;
/**
 * Parse `JSON` and handles invalid `JSON` strings
 * @param str `JSON` string
 */
function parseJson(str) {
    if (str.length === 0) {
        // special-case empty json body, as it's a common client-side mistake
        return {};
    }
    try {
        return JSON.parse(str);
    }
    catch (e) {
        throw new ApiError(400, 'Invalid JSON');
    }
}
/**
 * Parsing query arguments from request `url` string
 * @param url of request
 * @returns Object with key name of query argument and its value
 */
function getQueryParser({ url }) {
    return function parseQuery() {
        const { URL } = require('url');
        // we provide a placeholder base url because we only want searchParams
        const params = new URL(url, 'https://n').searchParams;
        const query = {};
        for (const [key, value] of params) {
            if (query[key]) {
                if (Array.isArray(query[key])) {
                    ;
                    query[key].push(value);
                }
                else {
                    query[key] = [query[key], value];
                }
            }
            else {
                query[key] = value;
            }
        }
        return query;
    };
}
exports.getQueryParser = getQueryParser;
/**
 * Parse cookies from `req` header
 * @param req request object
 */
function getCookieParser(req) {
    return function parseCookie() {
        const header = req.headers.cookie;
        if (!header) {
            return {};
        }
        const { parse } = require('next/dist/compiled/cookie');
        return parse(Array.isArray(header) ? header.join(';') : header);
    };
}
exports.getCookieParser = getCookieParser;
/**
 *
 * @param res response object
 * @param statusCode `HTTP` status code of response
 */
function sendStatusCode(res, statusCode) {
    res.statusCode = statusCode;
    return res;
}
exports.sendStatusCode = sendStatusCode;
/**
 * Send `any` body to response
 * @param res response object
 * @param body of response
 */
function sendData(res, body) {
    if (body === null) {
        res.end();
        return;
    }
    const contentType = res.getHeader('Content-Type');
    if (Buffer.isBuffer(body)) {
        if (!contentType) {
            res.setHeader('Content-Type', 'application/octet-stream');
        }
        res.setHeader('Content-Length', body.length);
        res.end(body);
        return;
    }
    if (body instanceof stream_1.Stream) {
        if (!contentType) {
            res.setHeader('Content-Type', 'application/octet-stream');
        }
        body.pipe(res);
        return;
    }
    let str = body;
    // Stringify JSON body
    if (typeof body === 'object' ||
        typeof body === 'number' ||
        typeof body === 'boolean') {
        str = JSON.stringify(body);
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
    }
    res.setHeader('Content-Length', Buffer.byteLength(str));
    res.end(str);
}
exports.sendData = sendData;
/**
 * Send `JSON` object
 * @param res response object
 * @param jsonBody of data
 */
function sendJson(res, jsonBody) {
    // Set header to application/json
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    // Use send to handle request
    res.send(jsonBody);
}
exports.sendJson = sendJson;
const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`;
const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data`;
exports.SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA);
const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS);
function tryGetPreviewData(req, res, options) {
    // Read cached preview data if present
    if (exports.SYMBOL_PREVIEW_DATA in req) {
        return req[exports.SYMBOL_PREVIEW_DATA];
    }
    const getCookies = getCookieParser(req);
    let cookies;
    try {
        cookies = getCookies();
    }
    catch (_a) {
        // TODO: warn
        return false;
    }
    const hasBypass = COOKIE_NAME_PRERENDER_BYPASS in cookies;
    const hasData = COOKIE_NAME_PRERENDER_DATA in cookies;
    // Case: neither cookie is set.
    if (!(hasBypass || hasData)) {
        return false;
    }
    // Case: one cookie is set, but not the other.
    if (hasBypass !== hasData) {
        clearPreviewData(res);
        return false;
    }
    // Case: preview session is for an old build.
    if (cookies[COOKIE_NAME_PRERENDER_BYPASS] !== options.previewModeId) {
        clearPreviewData(res);
        return false;
    }
    const tokenPreviewData = cookies[COOKIE_NAME_PRERENDER_DATA];
    const jsonwebtoken = require('next/dist/compiled/jsonwebtoken');
    let encryptedPreviewData;
    try {
        encryptedPreviewData = jsonwebtoken.verify(tokenPreviewData, options.previewModeSigningKey);
    }
    catch (_b) {
        // TODO: warn
        clearPreviewData(res);
        return false;
    }
    const decryptedPreviewData = crypto_utils_1.decryptWithSecret(Buffer.from(options.previewModeEncryptionKey), encryptedPreviewData.data);
    try {
        // TODO: strict runtime type checking
        const data = JSON.parse(decryptedPreviewData);
        // Cache lookup
        Object.defineProperty(req, exports.SYMBOL_PREVIEW_DATA, {
            value: data,
            enumerable: false,
        });
        return data;
    }
    catch (_c) {
        return false;
    }
}
exports.tryGetPreviewData = tryGetPreviewData;
function setPreviewData(res, data, // TODO: strict runtime type checking
options) {
    if (typeof options.previewModeId !== 'string' ||
        options.previewModeId.length < 16) {
        throw new Error('invariant: invalid previewModeId');
    }
    if (typeof options.previewModeEncryptionKey !== 'string' ||
        options.previewModeEncryptionKey.length < 16) {
        throw new Error('invariant: invalid previewModeEncryptionKey');
    }
    if (typeof options.previewModeSigningKey !== 'string' ||
        options.previewModeSigningKey.length < 16) {
        throw new Error('invariant: invalid previewModeSigningKey');
    }
    const jsonwebtoken = require('next/dist/compiled/jsonwebtoken');
    const payload = jsonwebtoken.sign({
        data: crypto_utils_1.encryptWithSecret(Buffer.from(options.previewModeEncryptionKey), JSON.stringify(data)),
    }, options.previewModeSigningKey, Object.assign({ algorithm: 'HS256' }, (options.maxAge !== undefined
        ? { expiresIn: options.maxAge }
        : undefined)));
    // limit preview mode cookie to 2KB since we shouldn't store too much
    // data here and browsers drop cookies over 4KB
    if (payload.length > 2048) {
        throw new Error(`Preview data is limited to 2KB currently, reduce how much data you are storing as preview data to continue`);
    }
    const { serialize, } = require('next/dist/compiled/cookie');
    const previous = res.getHeader('Set-Cookie');
    res.setHeader(`Set-Cookie`, [
        ...(typeof previous === 'string'
            ? [previous]
            : Array.isArray(previous)
                ? previous
                : []),
        serialize(COOKIE_NAME_PRERENDER_BYPASS, options.previewModeId, Object.assign({ httpOnly: true, sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax', secure: process.env.NODE_ENV !== 'development', path: '/' }, (options.maxAge !== undefined
            ? { maxAge: options.maxAge }
            : undefined))),
        serialize(COOKIE_NAME_PRERENDER_DATA, payload, Object.assign({ httpOnly: true, sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax', secure: process.env.NODE_ENV !== 'development', path: '/' }, (options.maxAge !== undefined
            ? { maxAge: options.maxAge }
            : undefined))),
    ]);
    return res;
}
function clearPreviewData(res) {
    if (SYMBOL_CLEARED_COOKIES in res) {
        return res;
    }
    const { serialize, } = require('next/dist/compiled/cookie');
    const previous = res.getHeader('Set-Cookie');
    res.setHeader(`Set-Cookie`, [
        ...(typeof previous === 'string'
            ? [previous]
            : Array.isArray(previous)
                ? previous
                : []),
        serialize(COOKIE_NAME_PRERENDER_BYPASS, '', {
            // To delete a cookie, set `expires` to a date in the past:
            // https://tools.ietf.org/html/rfc6265#section-4.1.1
            // `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
            expires: new Date(0),
            httpOnly: true,
            sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
            secure: process.env.NODE_ENV !== 'development',
            path: '/',
        }),
        serialize(COOKIE_NAME_PRERENDER_DATA, '', {
            // To delete a cookie, set `expires` to a date in the past:
            // https://tools.ietf.org/html/rfc6265#section-4.1.1
            // `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
            expires: new Date(0),
            httpOnly: true,
            sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
            secure: process.env.NODE_ENV !== 'development',
            path: '/',
        }),
    ]);
    Object.defineProperty(res, SYMBOL_CLEARED_COOKIES, {
        value: true,
        enumerable: false,
    });
    return res;
}
/**
 * Custom error class
 */
class ApiError extends Error {
    constructor(statusCode, message) {
        super(message);
        this.statusCode = statusCode;
    }
}
exports.ApiError = ApiError;
/**
 * Sends error in `response`
 * @param res response object
 * @param statusCode of response
 * @param message of response
 */
function sendError(res, statusCode, message) {
    res.statusCode = statusCode;
    res.statusMessage = message;
    res.end(message);
}
exports.sendError = sendError;
/**
 * Execute getter function only if its needed
 * @param LazyProps `req` and `params` for lazyProp
 * @param prop name of property
 * @param getter function to get data
 */
function setLazyProp({ req, params }, prop, getter) {
    const opts = { configurable: true, enumerable: true };
    const optsReset = Object.assign(Object.assign({}, opts), { writable: true });
    Object.defineProperty(req, prop, Object.assign(Object.assign({}, opts), { get: () => {
            let value = getter();
            if (params && typeof params !== 'boolean') {
                value = Object.assign(Object.assign({}, value), params);
            }
            // we set the property on the object to avoid recalculating it
            Object.defineProperty(req, prop, Object.assign(Object.assign({}, optsReset), { value }));
            return value;
        }, set: value => {
            Object.defineProperty(req, prop, Object.assign(Object.assign({}, optsReset), { value }));
        } }));
}
exports.setLazyProp = setLazyProp;
