import { createHash, randomBytes } from "crypto";
import createHttpError from "http-errors";

export function doubleCsrf({ getSecret, getSessionIdentifier = () => "", cookieName = "__Host-psifi.x-csrf-token", cookieOptions: { sameSite = "lax", path = "/", secure = true, httpOnly = true, ...remainingCookieOptions } = {}, delimiter = "|", size = 64, ignoredMethods = ["GET", "HEAD", "OPTIONS"], getTokenFromRequest = (req) => req.headers["x-csrf-token"], errorConfig: { statusCode = 403, message = "invalid csrf token", code = "EBADCSRFTOKEN", } = {}, }) {
    const ignoredMethodsSet = new Set(ignoredMethods);
    const cookieOptions = {
        sameSite,
        path,
        secure,
        httpOnly,
        ...remainingCookieOptions,
    };
    const invalidCsrfTokenError = createHttpError(statusCode, message, {
        code: code,
    });
    const generateTokenAndHash = (req, overwrite, validateOnReuse) => {
        const getSecretResult = getSecret(req);
        const possibleSecrets = Array.isArray(getSecretResult)
            ? getSecretResult
            : [getSecretResult];
        const csrfCookie = getCsrfCookieFromRequest(req);
        if (typeof csrfCookie === "string" && !overwrite) {
            const [csrfToken, csrfTokenHash] = csrfCookie.split(delimiter);
            if (validateTokenAndHashPair({
                csrfToken,
                csrfTokenHash,
                possibleSecrets,
                sessionIdentifier: getSessionIdentifier(req),
            })) {
                return { csrfToken, csrfTokenHash };
            }
            else if (validateOnReuse) {
                throw invalidCsrfTokenError;
            }
        }
        const csrfToken = randomBytes(size).toString("hex");
        const secret = possibleSecrets[0];
        const csrfTokenHash = createHash("sha256")
            .update(`${getSessionIdentifier(req)}${csrfToken}${secret}`)
            .digest("hex");
        return { csrfToken, csrfTokenHash };
    };
    const generateToken = (req, res, overwrite = false, validateOnReuse = true) => {
        const { csrfToken, csrfTokenHash } = generateTokenAndHash(req, overwrite, validateOnReuse);
        const cookieContent = `${csrfToken}${delimiter}${csrfTokenHash}`;
        res.cookie(cookieName, cookieContent, { ...cookieOptions });
        return csrfToken;
    };
    const getCsrfCookieFromRequest = remainingCookieOptions.signed
        ? (req) => req.signedCookies[cookieName]
        : (req) => req.cookies[cookieName];
    const validateTokenAndHashPair = ({ csrfToken, csrfTokenHash, possibleSecrets, sessionIdentifier, }) => {
        if (typeof csrfToken !== "string" || typeof csrfTokenHash !== "string")
            return false;
        for (const secret of possibleSecrets) {
            const expectedHash = createHash("sha256")
                .update(`${sessionIdentifier}${csrfToken}${secret}`)
                .digest("hex");
            if (csrfTokenHash === expectedHash)
                return true;
        }
        return false;
    };
    const validateRequest = (req) => {
        const csrfCookie = getCsrfCookieFromRequest(req);
        if (typeof csrfCookie !== "string")
            return false;
        const [csrfToken, csrfTokenHash] = csrfCookie.split(delimiter);
        const csrfTokenFromRequest = getTokenFromRequest(req);
        const getSecretResult = getSecret(req);
        const possibleSecrets = Array.isArray(getSecretResult)
            ? getSecretResult
            : [getSecretResult];
        return (csrfToken === csrfTokenFromRequest &&
            validateTokenAndHashPair({
                csrfToken: csrfTokenFromRequest,
                csrfTokenHash,
                possibleSecrets,
                sessionIdentifier: getSessionIdentifier(req),
            }));
    };
    const doubleCsrfProtection = (req, res, next) => {
        req.csrfToken = (overwrite, validateOnReuse) => generateToken(req, res, overwrite, validateOnReuse);
        if (ignoredMethodsSet.has(req.method)) {
            next();
        }
        else if (validateRequest(req)) {
            next();
        }
        else {
            next(invalidCsrfTokenError);
        }
    };
    return {
        invalidCsrfTokenError,
        generateToken,
        validateRequest,
        doubleCsrfProtection,
    };
}
