Files
zkldi_Tachi/server/src/lib/setup/config.ts
T
zkldi 7eea518336 fix: change prudence's default export to named
this was causing issues with vite and is potentially a bug in prudence
or in vite. it's very hard to tell and i've already spent
3 hours trying to fix this, so here.
2022-12-23 09:04:32 +00:00

343 lines
8.8 KiB
TypeScript

/* eslint-disable no-console */
// barrel file for re-exporting env variables.
import dotenv from "dotenv";
import JSON5 from "json5";
import { p } from "prudence";
import { StaticConfig } from "tachi-common";
import { FormatPrError } from "utils/prudence";
import fs from "fs";
import { URL } from "url";
import type { SendMailOptions } from "nodemailer";
import type { Game, ImportTypes, integer } from "tachi-common";
// imports things like NODE_ENV from a local .env file if one is present.
dotenv.config();
// stub - having a real logger here creates a circular dependency.
const logger = console;
const confLocation = process.env.TCHIS_CONF_LOCATION ?? "./conf.json5";
// reads from $pwd/conf.json5, unless an override is set
let confFile;
try {
confFile = fs.readFileSync(confLocation, "utf-8");
} catch (err) {
logger.error("Error while trying to open conf.json5. Is one present?", { err });
process.exit(1);
}
const config: unknown = JSON5.parse(confFile);
function isValidURL(self: unknown) {
if (typeof self !== "string") {
return `Expected URL, received type ${typeof self}`;
}
try {
// eslint-disable-next-line no-new
new URL(self);
return true;
} catch (err) {
return `Invalid URL ${self} (${(err as Error).message}).`;
}
}
export interface OAuth2Info {
CLIENT_ID: string;
CLIENT_SECRET: string;
REDIRECT_URI: string;
}
export interface CGConfig {
API_KEY: string;
URL: string;
}
export interface TachiServerConfig {
MONGO_DATABASE_NAME: string;
CAPTCHA_SECRET_KEY: string;
SESSION_SECRET: string;
FLO_API_URL?: string;
EAG_API_URL?: string;
MIN_API_URL?: string;
ARC_API_URL?: string;
CG_DEV_CONFIG?: CGConfig;
CG_PROD_CONFIG?: CGConfig;
FLO_OAUTH2_INFO?: OAuth2Info;
EAG_OAUTH2_INFO?: OAuth2Info;
MIN_OAUTH2_INFO?: OAuth2Info;
ARC_AUTH_TOKEN?: string;
ENABLE_SERVER_HTTPS?: boolean;
CLIENT_DEV_SERVER?: string | null;
RATE_LIMIT: integer;
OAUTH_CLIENT_CAP: integer;
OPTIONS_ALWAYS_SUCCEEDS?: boolean;
USE_EXTERNAL_SCORE_IMPORT_WORKER: boolean;
EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY?: integer;
SEEDS_CONFIG?: {
REPO_URL: string;
USER_NAME: string | null;
USER_EMAIL: string | null;
};
EMAIL_CONFIG?: {
FROM: string;
DKIM?: SendMailOptions["dkim"];
// @warning This is explicitly allowed to be any
// As nodemailer doesnt properly export the types we care about
// This should be set to SMTPTransport.Options, but it is
// inaccessible.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TRANSPORT_OPS: any;
};
USC_QUEUE_SIZE: integer;
BEATORAJA_QUEUE_SIZE: integer;
MAX_GOAL_SUBSCRIPTIONS: integer;
MAX_QUEST_SUBSCRIPTIONS: integer;
MAX_FOLLOWING_AMOUNT: integer;
MAX_RIVALS: integer;
OUR_URL: string;
ALLOW_RUNNING_OFFLINE?: boolean;
INVITE_CODE_CONFIG?: {
BATCH_SIZE: integer;
INVITE_CAP: integer;
BETA_USER_BONUS: integer;
};
TACHI_CONFIG: {
NAME: string;
TYPE: "btchi" | "ktchi" | "omni";
GAMES: Array<Game>;
IMPORT_TYPES: Array<ImportTypes>;
};
LOGGER_CONFIG: {
LOG_LEVEL: "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn";
CONSOLE: boolean;
FILE: boolean;
SEQ_API_KEY: string | undefined;
DISCORD?: {
WEBHOOK_URL: string;
WHO_TO_TAG: Array<string>;
};
};
CDN_CONFIG: {
WEB_LOCATION: string;
SAVE_LOCATION:
| {
TYPE: "S3_BUCKET";
ENDPOINT: string;
ACCESS_KEY_ID: string;
SECRET_ACCESS_KEY: string;
BUCKET: string;
KEY_PREFIX?: string;
REGION?: string;
}
| { TYPE: "LOCAL_FILESYSTEM"; LOCATION: string; SERVE_OWN_CDN?: boolean };
};
}
const isValidOauth2 = p.optional({
CLIENT_ID: "string",
CLIENT_SECRET: "string",
REDIRECT_URI: "string",
});
const isValidCGConfig = p.optional({
API_KEY: "string",
URL: "string",
});
const err = p(config, {
MONGO_DATABASE_NAME: "string",
CAPTCHA_SECRET_KEY: "string",
SESSION_SECRET: "string",
FLO_API_URL: p.optional(isValidURL),
EAG_API_URL: p.optional(isValidURL),
MIN_API_URL: p.optional(isValidURL),
ARC_API_URL: p.optional(isValidURL),
CG_DEV_CONFIG: isValidCGConfig,
CG_PROD_CONFIG: isValidCGConfig,
FLO_OAUTH2_INFO: isValidOauth2,
EAG_OAUTH2_INFO: isValidOauth2,
MIN_OAUTH2_INFO: isValidOauth2,
ARC_AUTH_TOKEN: "*string",
ENABLE_SERVER_HTTPS: "*boolean",
CLIENT_DEV_SERVER: "*?string",
RATE_LIMIT: p.optional(p.isPositiveInteger),
OAUTH_CLIENT_CAP: p.optional(p.isPositiveInteger),
OPTIONS_ALWAYS_SUCCEEDS: "*boolean",
USE_EXTERNAL_SCORE_IMPORT_WORKER: "*boolean",
EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY: p.optional(p.isPositiveInteger),
ALLOW_RUNNING_OFFLINE: "*boolean",
EMAIL_CONFIG: p.optional({
FROM: "string",
DKIM: "*object",
// WARN: This validation is improper and lazy.
// The actual content is just some wacky options object.
// I'm not going to assert this properly.
TRANSPORT_OPS: "*object",
}),
USC_QUEUE_SIZE: p.optional(p.gteInt(2)),
BEATORAJA_QUEUE_SIZE: p.optional(p.gteInt(2)),
MAX_GOAL_SUBSCRIPTIONS: p.optional(p.isPositiveInteger),
MAX_QUEST_SUBSCRIPTIONS: p.optional(p.isPositiveInteger),
MAX_FOLLOWING_AMOUNT: p.optional(p.isPositiveInteger),
MAX_RIVALS: p.optional(p.isPositiveInteger),
OUR_URL: (self) => {
if (typeof self !== "string") {
return "Expected a string.";
}
if (self.endsWith("/")) {
return `OUR_URL should not end with a trailing slash. Use ${self.substring(
0,
self.length - 1
)} instead.`;
}
return true;
},
INVITE_CODE_CONFIG: p.optional({
BATCH_SIZE: p.isPositiveInteger,
INVITE_CAP: p.isPositiveInteger,
BETA_USER_BONUS: p.isPositiveInteger,
}),
TACHI_CONFIG: {
NAME: "string",
TYPE: p.isIn("ktchi", "btchi", "omni"),
GAMES: [p.isIn(StaticConfig.allSupportedGames)],
IMPORT_TYPES: [p.isIn(StaticConfig.allImportTypes)],
},
LOGGER_CONFIG: {
LOG_LEVEL: p.optional(
p.isIn("debug", "verbose", "info", "warn", "error", "severe", "crit")
),
CONSOLE: "*boolean",
FILE: "*boolean",
SEQ_API_KEY: "*string",
DISCORD: p.optional({
WEBHOOK_URL: "string",
WHO_TO_TAG: ["string"],
}),
},
CDN_CONFIG: {
WEB_LOCATION: "string",
SAVE_LOCATION: p.or(
{
TYPE: p.is("LOCAL_FILESYSTEM"),
SERVE_OWN_CDN: "*boolean",
LOCATION: "string",
},
{
TYPE: p.is("S3_BUCKET"),
ENDPOINT: "string",
ACCESS_KEY_ID: "string",
SECRET_ACCESS_KEY: "string",
BUCKET: "string",
KEY_PREFIX: "*string",
REGION: "*string",
}
),
},
SEEDS_CONFIG: p.optional({
REPO_URL: "string",
USER_NAME: "?string",
USER_EMAIL: "?string",
}),
});
if (err) {
throw new Error(FormatPrError(err, "Invalid conf.json5 file."));
}
const tachiServerConfig = config as TachiServerConfig;
// default rate limit 500
tachiServerConfig.RATE_LIMIT ??= 500;
tachiServerConfig.OAUTH_CLIENT_CAP ??= 15;
tachiServerConfig.USC_QUEUE_SIZE ??= 3;
tachiServerConfig.BEATORAJA_QUEUE_SIZE ??= 3;
tachiServerConfig.MAX_GOAL_SUBSCRIPTIONS ??= 1_000;
tachiServerConfig.MAX_QUEST_SUBSCRIPTIONS ??= 100;
tachiServerConfig.MAX_RIVALS ??= 5;
tachiServerConfig.MAX_FOLLOWING_AMOUNT ??= 1_000;
tachiServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER ??= false;
export const TachiConfig = tachiServerConfig.TACHI_CONFIG;
export const ServerConfig = tachiServerConfig;
// Environment Variable Validation
let port = Number(process.env.PORT);
if (Number.isNaN(port) && process.env.IS_SERVER) {
logger.warn(`No/invalid PORT specified in environment, defaulting to 8080.`);
port = 8080;
}
const redisUrl = process.env.REDIS_URL ?? "";
if (!redisUrl) {
// n.b. These logs should be critical level, but the logger cant actually instantiate
// itself in this file, because this file also controlls the logger. Ouch!
logger.error(`No REDIS_URL specified in environment. Terminating.`);
process.exit(1);
}
const mongoUrl = process.env.MONGO_URL ?? "";
if (!mongoUrl) {
logger.error(`No MONGO_URL specified in environment. Terminating.`);
process.exit(1);
}
const seqUrl = process.env.SEQ_URL ?? "";
if (!seqUrl && tachiServerConfig.LOGGER_CONFIG.SEQ_API_KEY) {
logger.warn(
`No SEQ_URL specified in environment, yet LOGGER_CONFIG.SEQ_API_KEY was defined. No logs will be sent to Seq!`
);
}
const nodeEnv = process.env.NODE_ENV ?? "";
if (!nodeEnv) {
logger.error(`No NODE_ENV specified in environment. Terminating.`);
process.exit(1);
}
if (!["dev", "production", "staging", "test"].includes(nodeEnv)) {
logger.error(
`Invalid NODE_ENV set in environment. Expected dev, production, test or staging. Got ${nodeEnv}.`
);
process.exit(1);
}
// if (bms XOR popn) is enabled
if (TachiConfig.GAMES.includes("bms") !== TachiConfig.GAMES.includes("pms")) {
logger.error(
`BMS and PMS MUST be enabled at the same time, due to how the beatoraja IR works.`
);
process.exit(1);
}
const replicaIdentity = process.env.REPLICA_IDENTITY;
export const Environment = {
port,
redisUrl,
mongoUrl,
nodeEnv: nodeEnv as "dev" | "production" | "staging" | "test",
replicaIdentity,
seqUrl,
commitHash: process.env.COMMIT_HASH,
};