"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// Native
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
// Packages
const json_1 = require("klona/json");
const joi_1 = __importDefault(require("joi"));
const cosmiconfig_1 = require("cosmiconfig");
const yargs_1 = require("yargs");
// Ours
const logger_interface_1 = require("../../shared/logger-interface");
function getConfigSchema(userConfig) {
    var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12;
    return joi_1.default.object({
        host: joi_1.default.string().default('0.0.0.0').description('The IP address or hostname that NodeCG should bind to.'),
        port: joi_1.default.number().port().default(9090).description('The port that NodeCG should listen on.'),
        baseURL: joi_1.default.string()
            .default('')
            .description('The URL of this instance. Used for things like cookies. Defaults to HOST:PORT. ' +
            "If you use a reverse proxy, you'll likely need to set this value."),
        logging: joi_1.default.object({
            console: joi_1.default.object({
                enabled: joi_1.default.boolean().default(true).description('Whether to enable console logging.'),
                level: joi_1.default.string()
                    .valid(...Object.values(logger_interface_1.LogLevel))
                    .default('info'),
                timestamps: joi_1.default.boolean().default(true).description('Whether to add timestamps to the console logging.'),
                replicants: joi_1.default.boolean()
                    .default(false)
                    .description('Whether to enable logging of the Replicants subsystem. Very spammy.'),
            }).default(),
            file: joi_1.default.object({
                enabled: joi_1.default.boolean().default(false).description('Whether to enable file logging.'),
                level: joi_1.default.string()
                    .valid(...Object.values(logger_interface_1.LogLevel))
                    .default('info'),
                path: joi_1.default.string().default('logs/nodecg.log').description('The filepath to log to.'),
                timestamps: joi_1.default.boolean().default(true).description('Whether to add timestamps to the file logging.'),
                replicants: joi_1.default.boolean()
                    .default(false)
                    .description('Whether to enable logging of the Replicants subsystem. Very spammy.'),
            }).default(),
        }).default(),
        bundles: joi_1.default.object({
            enabled: joi_1.default.array()
                .items(joi_1.default.string())
                .allow(null)
                .default((_a = yargs_1.argv['bundlesEnabled']) !== null && _a !== void 0 ? _a : null)
                .description('A whitelist array of bundle names that will be the only ones loaded at startup.'),
            disabled: joi_1.default.array()
                .items(joi_1.default.string())
                .allow(null)
                .default((_b = yargs_1.argv['bundlesDisabled']) !== null && _b !== void 0 ? _b : null)
                .description('A blacklist array of bundle names that will be excluded from loading at startup.'),
            paths: joi_1.default.array()
                .items(joi_1.default.string())
                .default((_c = yargs_1.argv['bundlesPaths']) !== null && _c !== void 0 ? _c : [])
                .description('An array of additional paths where bundles are located.'),
        }).default({
            enabled: null,
            disabled: null,
            paths: [],
        }),
        login: joi_1.default.object({
            enabled: joi_1.default.boolean().default(false).description('Whether to enable login security.'),
            sessionSecret: joi_1.default.string()
                // This will throw if the user does not provide a value, but only if login security is enabled.
                .default(((_d = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _d === void 0 ? void 0 : _d.enabled) ? null : 'insecureButUnused')
                .description('The secret used to salt sessions.'),
            forceHttpsReturn: joi_1.default.boolean()
                .default(false)
                .description('Forces Steam & Twitch login return URLs to use HTTPS instead of HTTP. Useful in reverse proxy setups.'),
            steam: joi_1.default.object({
                enabled: joi_1.default.boolean().default(false).description('Whether to enable Steam authentication.'),
                apiKey: joi_1.default.string()
                    // This will throw if the user does not provide a value, but only if Steam auth is enabled.
                    .default(((_f = (_e = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _e === void 0 ? void 0 : _e.steam) === null || _f === void 0 ? void 0 : _f.enabled) ? null : '')
                    .description('A Steam API Key. Obtained from http://steamcommunity.com/dev/apikey'),
                allowedIds: joi_1.default.array()
                    .items(joi_1.default.string())
                    // This will throw if the user does not provide a value, but only if Steam auth is enabled.
                    .default(((_h = (_g = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _g === void 0 ? void 0 : _g.steam) === null || _h === void 0 ? void 0 : _h.enabled) ? null : [])
                    .description('Which 64 bit Steam IDs to allow. Can be obtained from https://steamid.io/'),
            }),
            twitch: joi_1.default.object({
                enabled: joi_1.default.boolean().default(false).description('Whether to enable Twitch authentication.'),
                clientID: joi_1.default.string()
                    // This will throw if the user does not provide a value, but only if Twitch auth is enabled.
                    .default(((_k = (_j = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _j === void 0 ? void 0 : _j.twitch) === null || _k === void 0 ? void 0 : _k.enabled) ? null : '')
                    .description('A Twitch application ClientID http://twitch.tv/kraken/oauth2/clients/new'),
                clientSecret: joi_1.default.string()
                    // This will throw if the user does not provide a value, but only if Twitch auth is enabled.
                    .default(((_m = (_l = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _l === void 0 ? void 0 : _l.twitch) === null || _m === void 0 ? void 0 : _m.enabled) ? null : '')
                    .description('A Twitch application ClientSecret http://twitch.tv/kraken/oauth2/clients/new'),
                scope: joi_1.default.string()
                    .default('user_read')
                    .description('A space-separated string of Twitch application permissions.'),
                allowedUsernames: joi_1.default.array()
                    .items(joi_1.default.string())
                    // This will throw if the user does not provide a value and is not using allowedIds, but only if Twitch auth is enabled.
                    .default(((_p = (_o = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _o === void 0 ? void 0 : _o.twitch) === null || _p === void 0 ? void 0 : _p.enabled) && !((_r = (_q = userConfig === null || userConfig === void 0 ? void 0 : userConfig['']) === null || _q === void 0 ? void 0 : _q.twitch) === null || _r === void 0 ? void 0 : _r.allowedIds) ? null : [])
                    .description('Which Twitch usernames to allow.'),
                allowedIds: joi_1.default.array()
                    .items(joi_1.default.string())
                    // This will throw if the user does not provide a value and is not using allowedUsernames, but only if Twitch auth is enabled.
                    .default(((_t = (_s = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _s === void 0 ? void 0 : _s.twitch) === null || _t === void 0 ? void 0 : _t.enabled) && !((_v = (_u = userConfig === null || userConfig === void 0 ? void 0 : userConfig['']) === null || _u === void 0 ? void 0 : _u.twitch) === null || _v === void 0 ? void 0 : _v.allowedUsernames) ? null : [])
                    .description('Which Twitch IDs to allow. Can be obtained from https://twitchinsights.net/checkuser'),
            }),
            discord: joi_1.default.object({
                enabled: joi_1.default.boolean().default(false).description('Whether to enable Discord authentication.'),
                clientID: joi_1.default.string()
                    // This will throw if the user does not provide a value, but only if Discord auth is enabled.
                    .default(((_x = (_w = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _w === void 0 ? void 0 : _w.discord) === null || _x === void 0 ? void 0 : _x.enabled) ? null : '')
                    .description('A Discord application ClientID https://discord.com/developers/applications'),
                clientSecret: joi_1.default.string()
                    // This will throw if the user does not provide a value, but only if Discord auth is enabled.
                    .default(((_z = (_y = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _y === void 0 ? void 0 : _y.discord) === null || _z === void 0 ? void 0 : _z.enabled) ? null : '')
                    .description('A Discord application ClientSecret https://discord.com/developers/applications'),
                scope: joi_1.default.string()
                    .default('identify')
                    .description('A space-separated string of Discord application scopes. https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes'),
                allowedUserIDs: joi_1.default.array()
                    .items(joi_1.default.string())
                    // This will throw if the user does not provide a value and is not using allowedGuilds, but only if Discord auth is enabled.
                    .default(((_1 = (_0 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _0 === void 0 ? void 0 : _0.discord) === null || _1 === void 0 ? void 0 : _1.enabled) && !((_3 = (_2 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _2 === void 0 ? void 0 : _2.discord) === null || _3 === void 0 ? void 0 : _3.allowedGuilds) ? null : [])
                    .description('Which Discord user IDs to allow.'),
                allowedGuilds: joi_1.default.array()
                    .items(joi_1.default.object({
                    guildID: joi_1.default.string().description('Users in this Discord Server are allowed to log in.'),
                    allowedRoleIDs: joi_1.default.array()
                        .items(joi_1.default.string())
                        .default([])
                        .description('Additionally require one of the roles on the server to log in.'),
                    guildBotToken: joi_1.default.string()
                        .default('')
                        .description('Discord bot token, needed if allowedRoleIDs is used.'),
                }))
                    // This will throw if the user does not provide a value and is not using allowedUserIDs, but only if Discord auth is enabled.
                    .default(((_5 = (_4 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _4 === void 0 ? void 0 : _4.discord) === null || _5 === void 0 ? void 0 : _5.enabled) && !((_7 = (_6 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _6 === void 0 ? void 0 : _6.discord) === null || _7 === void 0 ? void 0 : _7.allowedUserIDs) ? null : []),
            }),
            local: joi_1.default.object({
                enabled: joi_1.default.boolean().default(false).description('Enable Local authentication.'),
                allowedUsers: joi_1.default.array()
                    .items(joi_1.default.object({
                    username: joi_1.default.string(),
                    password: joi_1.default.string(),
                }))
                    // This will throw if the user does not provide a value, but only if Local auth is enabled.
                    .default(((_9 = (_8 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['login']) === null || _8 === void 0 ? void 0 : _8.local) === null || _9 === void 0 ? void 0 : _9.enabled) ? null : [])
                    .description('Which users can log in.'),
            }),
        }).default(),
        ssl: joi_1.default.object({
            enabled: joi_1.default.boolean().default(false).description('Whether to enable SSL/HTTPS encryption.'),
            allowHTTP: joi_1.default.boolean()
                .default(false)
                .description('Whether to allow insecure HTTP connections while SSL is active.'),
            keyPath: joi_1.default.string()
                // This will throw if the user does not provide a value, but only if SSL is enabled.
                .default(((_10 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['ssl']) === null || _10 === void 0 ? void 0 : _10.enabled) ? null : '')
                .description('The path to an SSL key file.'),
            certificatePath: joi_1.default.string()
                // This will throw if the user does not provide a value, but only if SSL is enabled.
                .default(((_11 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['ssl']) === null || _11 === void 0 ? void 0 : _11.enabled) ? null : '')
                .description('The path to an SSL certificate file.'),
            passphrase: joi_1.default.string().description('The passphrase for the provided key file.').optional(),
        }).optional(),
        sentry: joi_1.default.object({
            enabled: joi_1.default.boolean().default(false).description('Whether to enable Sentry error reporting.'),
            dsn: joi_1.default.string()
                // This will throw if the user does not provide a value, but only if Sentry is enabled.
                .default(((_12 = userConfig === null || userConfig === void 0 ? void 0 : userConfig['sentry']) === null || _12 === void 0 ? void 0 : _12.enabled) ? null : '')
                .description("Your project's DSN, used to route alerts to the correct place."),
        }).optional(),
    });
}
function default_1(cfgDirOrFile) {
    var _a, _b, _c, _d, _e, _f, _g, _h;
    let isFile = false;
    try {
        isFile = fs_1.default.lstatSync(cfgDirOrFile).isFile();
    }
    catch (error) {
        if (error.code !== 'ENOENT') {
            throw error;
        }
    }
    const cfgDir = isFile ? path_1.default.dirname(cfgDirOrFile) : cfgDirOrFile;
    const cc = (0, cosmiconfig_1.cosmiconfigSync)('nodecg', {
        searchPlaces: isFile
            ? [path_1.default.basename(cfgDirOrFile)]
            : ['nodecg.json', 'nodecg.yaml', 'nodecg.yml', 'nodecg.js', 'nodecg.config.js'],
        stopDir: cfgDir,
    });
    const result = cc.search(cfgDir);
    const userCfg = (_a = result === null || result === void 0 ? void 0 : result.config) !== null && _a !== void 0 ? _a : {};
    if (((_b = userCfg === null || userCfg === void 0 ? void 0 : userCfg.bundles) === null || _b === void 0 ? void 0 : _b.enabled) && ((_c = userCfg === null || userCfg === void 0 ? void 0 : userCfg.bundles) === null || _c === void 0 ? void 0 : _c.disabled)) {
        throw new Error('nodecg.json may only contain EITHER bundles.enabled OR bundles.disabled, not both.');
    }
    else if (!userCfg) {
        console.info('[nodecg] No config found, using defaults.');
    }
    const schema = getConfigSchema(userCfg);
    /**
     * Generate the config in two passes, because Joi is kind of weird.
     *
     * We apply defaults, but we need to do that in a separate pass
     * before we report validation errors.
     */
    const { value: cfgWithDefaults } = schema.validate(userCfg, {
        abortEarly: false,
        allowUnknown: true,
    });
    cfgWithDefaults.baseURL =
        cfgWithDefaults.baseURL ||
            `${cfgWithDefaults.host === '0.0.0.0' ? 'localhost' : String(cfgWithDefaults.host)}:${String(cfgWithDefaults.port)}`;
    const validationResult = schema.validate(cfgWithDefaults, {
        noDefaults: true,
    });
    if (validationResult.error) {
        if (!process.env.NODECG_TEST) {
            console.error('[nodecg] Config invalid:\n', validationResult.error.annotate());
        }
        throw new Error(validationResult.error.details[0].message);
    }
    const config = validationResult.value;
    if (!config) {
        if (!process.env.NODECG_TEST) {
            console.error('[nodecg] config unexpectedly undefined. This is a bug with NodeCG, not your config.');
        }
        throw new Error('config undefined');
    }
    // Create the filtered config
    const filteredConfig = {
        host: config.host,
        port: config.port,
        baseURL: config.baseURL,
        logging: {
            console: {
                enabled: config.logging.console.enabled,
                level: config.logging.console.level,
                timestamps: config.logging.console.timestamps,
                replicants: config.logging.console.replicants,
            },
            file: {
                enabled: config.logging.file.enabled,
                level: config.logging.file.level,
                timestamps: config.logging.file.timestamps,
                replicants: config.logging.file.replicants,
            },
        },
        login: {
            enabled: (_d = config.login) === null || _d === void 0 ? void 0 : _d.enabled,
        },
        sentry: {
            enabled: (_f = (_e = config.sentry) === null || _e === void 0 ? void 0 : _e.enabled) !== null && _f !== void 0 ? _f : false,
            dsn: (_h = (_g = config.sentry) === null || _g === void 0 ? void 0 : _g.dsn) !== null && _h !== void 0 ? _h : '',
        },
    };
    if (config.login.steam) {
        filteredConfig.login.steam = {
            enabled: config.login.steam.enabled,
        };
    }
    if (config.login.twitch) {
        filteredConfig.login.twitch = {
            enabled: config.login.twitch.enabled,
            clientID: config.login.twitch.clientID, // Validation wil have thrown if this is falsey.
            scope: config.login.twitch.scope,
        };
    }
    if (config.login.local) {
        filteredConfig.login.local = {
            enabled: config.login.local.enabled,
        };
    }
    if (config.login.discord) {
        filteredConfig.login.discord = {
            enabled: config.login.discord.enabled,
            clientID: config.login.discord.clientID, // Validation wil have thrown if this is falsey.
            scope: config.login.discord.scope,
        };
    }
    if (config.ssl) {
        filteredConfig.ssl = {
            enabled: config.ssl.enabled,
        };
    }
    return {
        config: (0, json_1.klona)(config),
        filteredConfig: (0, json_1.klona)(filteredConfig),
    };
}
exports.default = default_1;
//# sourceMappingURL=loader.js.map