refactor: config becomes zod&not shit

This commit is contained in:
zk
2026-03-12 19:59:38 +00:00
parent a0397fbaf6
commit fc3e2bf8e1
45 changed files with 285 additions and 418 deletions
+21 -5
View File
@@ -4,25 +4,41 @@
},
"eslint.validate": ["javascript", "javascriptreact", "html", "typescriptreact", "typescript"],
"search.useIgnoreFiles": true,
"editor.defaultFormatter": "biomejs.biome",
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
}
},
"[typescriptreact]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
}
},
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
}
},
"[json]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
}
},
"[jsonc]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
}
},
"files.associations": {
"Justfile-*": "makefile"
View File
+1 -2
View File
@@ -97,7 +97,6 @@
"node-fetch": "catalog:",
"prudence": "catalog:",
"seq-logging": "catalog:",
"sync-fetch": "catalog:",
"tachi-common": "workspace:*",
"ts-node": "catalog:",
"tsconfig-paths": "catalog:",
@@ -381,7 +380,6 @@
"connect-redis": "catalog:",
"csv-parse": "catalog:",
"deepmerge": "catalog:",
"dotenv": "catalog:",
"express": "catalog:",
"express-async-errors": "catalog:",
"express-prom-bundle": "catalog:",
@@ -417,6 +415,7 @@
"winston": "catalog:",
"winston-loki": "catalog:",
"winston-transport": "catalog:",
"zod": "catalog:",
},
"devDependencies": {
"@grpc/grpc-js": "catalog:",
-4
View File
@@ -430,7 +430,3 @@ this will log a warning, and nothing will be sent to Seq.
### NODE_ENV
Expected to be either "dev", "production", "staging" or "test". If not set, this will terminate the process.
### REPLICA_IDENTITY
Optional. If present, this declares the identity of this server as a replica.
+1
View File
@@ -4,6 +4,7 @@
"module": "Preserve",
"moduleResolution": "bundler",
"paths": {
"#*": ["./src/*"],
// lolololOLOloLOlooLOOOOOL
// LOLOLOLOLOLOLOL
"@discordjs/builders": ["../../node_modules/@discordjs/builders/dist/index"],
+4 -1
View File
@@ -5,7 +5,10 @@
"moduleResolution": "bundler",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "react-jsx",
"allowJs": true
"allowJs": true,
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"],
+4 -1
View File
@@ -2,7 +2,10 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "Preserve",
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
+1
View File
@@ -4,6 +4,7 @@
"module": "Preserve",
"moduleResolution": "bundler",
"paths": {
"#*": ["./src/*"],
"tachi-db-migration-engine": ["../db-migration-engine/src/index.ts"]
}
},
+4 -1
View File
@@ -8,7 +8,10 @@
"composite": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
"sourceMap": true,
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
+4 -1
View File
@@ -5,7 +5,10 @@
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"outDir": "js",
"rootDir": "src"
"rootDir": "src",
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
@@ -2,7 +2,10 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "Preserve",
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
+4 -1
View File
@@ -5,7 +5,10 @@
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"outDir": "js",
"rootDir": "src"
"rootDir": "src",
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
+4 -1
View File
@@ -6,7 +6,10 @@
"noImplicitAny": false,
"strictNullChecks": true,
"allowJs": true,
"strict": false
"strict": false,
"paths": {
"#*": ["./src/*"]
}
},
"exclude": ["node_modules"]
}
+1 -1
View File
@@ -75,7 +75,6 @@
"connect-redis": "catalog:",
"csv-parse": "catalog:",
"deepmerge": "catalog:",
"dotenv": "catalog:",
"express": "catalog:",
"express-async-errors": "catalog:",
"express-prom-bundle": "catalog:",
@@ -108,6 +107,7 @@
"ts-node": "catalog:",
"typescript": "catalog:",
"uuidv7": "catalog:",
"zod": "catalog:",
"winston": "catalog:",
"winston-loki": "catalog:",
"winston-transport": "catalog:"
+2 -2
View File
@@ -1,4 +1,4 @@
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import { expressRequestMock } from "#test-utils/mock-request";
import fs from "fs";
import path from "path";
@@ -17,7 +17,7 @@ function getTestTxt() {
}
const ResetFileRoot = () => {
if (Environment.nodeEnv !== "test") {
if (Env.NODE_ENV !== "test") {
throw new Error(
`Not in test, yet CDN.test.ts was triggered, which could rm -rf something important.`,
);
@@ -4,7 +4,7 @@ import fs from "fs";
// viz. boku and kamai share versioning. This is because they
// essentially share codebases.
import CreateLogCtx from "#lib/logger/logger";
import { Environment } from "#lib/setup/config";
import { Env } from "#lib/setup/config";
import path from "path";
import semver from "semver";
@@ -26,8 +26,8 @@ if (!semverInfo) {
let commit: string | null = null;
try {
if (Environment.commitHash) {
commit = Environment.commitHash;
if (Env.COMMIT_HASH) {
commit = Env.COMMIT_HASH;
} else {
// This fetches the HEAD of our current running branch. This is useful for debugging.
// Note that git returns this with a trailing newline, so we have to trim that off.
+2 -2
View File
@@ -1,5 +1,5 @@
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import bunyan from "bunyan";
import nodemailer, { type SentMessageInfo, type Transporter } from "nodemailer";
@@ -45,7 +45,7 @@ export function SendEmail(
htmlContent: string,
textContent: string,
): Promise<SentMessageInfo> | undefined {
if (Environment.nodeEnv === "test") {
if (Env.NODE_ENV === "test") {
logger.debug(`Stubbed out SendEmail as env was test.`);
return;
}
@@ -4,6 +4,7 @@ import type https from "https";
import { rootLogger } from "#lib/logger/logger";
import { CloseScoreImportQueue } from "#lib/score-import/worker/queue";
import { monkDB } from "#services/mongo/db";
import { ClosePgConnection } from "#services/pg/db.js";
import { CloseRedisConnection } from "#services/redis/redis";
const logger = rootLogger;
@@ -22,6 +23,9 @@ async function CloseEverythingElse() {
logger.info("Closing Mongo Database.", { shutdownInfo: true });
await monkDB.close();
logger.info("Closing database...", {shutdownInfo: true});
await ClosePgConnection();
logger.info("Closing Redis Connection.", { shutdownInfo: true });
await CloseRedisConnection();
@@ -1,9 +1,9 @@
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import t from "tap";
import CreateLogCtx, { ChangeRootLogLevel, GetLogLevel, rootLogger, Transports } from "./logger";
const LOG_LEVEL = Environment.logLevel;
const LOG_LEVEL = Env.LOG_LEVEL;
t.test("Logger Tests", (t) => {
const logger = CreateLogCtx(__filename);
+4 -32
View File
@@ -1,6 +1,5 @@
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
import { Env, TachiConfig } from "#lib/setup/config";
import { EscapeStringRegexp } from "#utils/misc";
import { Transport as SeqTransport } from "@valuabletouch/winston-seq";
import SafeJSONStringify from "safe-json-stringify";
import winston, { format, type LeveledLogMethod, type Logger, transports } from "winston";
@@ -66,18 +65,16 @@ const formatExcessPropertiesNoStack = (
return ` ${limit ? StrCap(content) : content}`;
};
const replicaInfo = Environment.replicaIdentity ? ` (${Environment.replicaIdentity})` : "";
const tachiPrintf = format.printf(
({ level, message, context = "tachi-root", timestamp, ...meta }) =>
`${timestamp}${replicaInfo} [${
`${timestamp} [${
Array.isArray(context) ? context.join(" | ") : context
}] ${level}: ${message}${formatExcessProperties(meta)}`,
);
const tachiConsolePrintf = format.printf(
({ level, message, context = "tachi-root", timestamp, hideFromConsole, ...meta }) =>
`${timestamp}${replicaInfo} [${
`${timestamp} [${
Array.isArray(context) ? context.join(" | ") : context
}] ${level}: ${message}${formatExcessPropertiesNoStack(
meta,
@@ -123,30 +120,6 @@ const tports: Array<winston.transport> = [
}),
];
if (Environment.seqUrl) {
const levelMap: Record<string, any> = {
crit: "Fatal",
severe: "Error",
error: "Error",
warn: "Warning",
info: "Information",
verbose: "Verbose",
debug: "Debug",
};
tports.push(
new SeqTransport({
apiKey: Environment.seqApiKey,
serverUrl: Environment.seqUrl,
onError: (err) => {
console.error(`Failed to send seq message: ${err.message}.`);
},
levelMapper(level = "") {
return levelMap[level] ?? "Information";
},
}),
);
}
export const rootLogger = winston.createLogger({
levels: {
@@ -177,7 +150,6 @@ export const rootLogger = winston.createLogger({
defaultMeta: {
__ServerName: TachiConfig.NAME,
__Worker: !!process.env.IS_WORKER,
__ReplicaID: Environment.replicaIdentity,
},
}) as KtLogger;
@@ -217,7 +189,7 @@ export function ChangeRootLogLevel(
export function GetLogLevel() {
return (
rootLogger.transports.map((e) => e.level).find((e) => typeof e === "string") ??
Environment.logLevel
Env.LOG_LEVEL
);
}
@@ -1,7 +1,7 @@
import type { Migration } from "#utils/types";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, TachiConfig } from "#lib/setup/config";
import { Env, TachiConfig } from "#lib/setup/config";
import db from "#services/mongo/db";
import UserFollowersMigration from "./migrations/add-following-to-users";
@@ -51,7 +51,7 @@ export const FAKE_MIGRATION: Migration = {
// If we're testing, we should pull fake migrations instead to ensure the tests
// stay consistent
const REGISTERED_MIGRATIONS: Array<Migration> =
Environment.nodeEnv === "test"
Env.NODE_ENV === "test"
? [FAKE_MIGRATION]
: [
UGPTRivalsMigration,
@@ -66,7 +66,7 @@ const REGISTERED_MIGRATIONS: Array<Migration> =
];
// only apply type-specific migrations if we're not in testing
if (Environment.nodeEnv !== "test") {
if (Env.NODE_ENV !== "test") {
// kamaitachi specific migrations
if (TachiConfig.TYPE !== "boku") {
REGISTERED_MIGRATIONS.push(
@@ -269,7 +269,7 @@ export async function ApplyMigration(migration: Migration) {
// remove the stale migration so it can be re-ran in the future.
await db.migrations.findOneAndDelete({ migrationID });
if (Environment.nodeEnv === "test") {
if (Env.NODE_ENV === "test") {
throw new Error("Was going to exit with statusCode 1, but we're in testing.");
}
@@ -1,8 +1,8 @@
import { Environment, TachiConfig } from "#lib/setup/config";
import { Env, TachiConfig } from "#lib/setup/config";
import { Queue, QueueEvents } from "bullmq";
const ScoreImportQueue = new Queue(`${TachiConfig.NAME} Score Import Queue`, {
connection: { host: Environment.redisUrl, port: 6379 },
connection: { host: Env.REDIS_URL, port: 6379 },
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 10, // keep the last 10 failed jobs, but start pruning beyond that.
@@ -12,7 +12,7 @@ const ScoreImportQueue = new Queue(`${TachiConfig.NAME} Score Import Queue`, {
export default ScoreImportQueue;
export const ScoreImportQueueEvents = new QueueEvents(ScoreImportQueue.name, {
connection: { host: Environment.redisUrl, port: 6379 },
connection: { host: Env.REDIS_URL, port: 6379 },
});
export async function CloseScoreImportQueue() {
@@ -1,6 +1,6 @@
import { HandleSIGTERMGracefully } from "#lib/handlers/sigterm";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import { FormatUserDoc, GetUserWithID } from "#utils/user";
import { Worker } from "bullmq";
import { EventEmitter } from "events";
@@ -130,7 +130,7 @@ export const worker = new Worker(
concurrency: ServerConfig.EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY ?? 10,
connection: {
port: 6379,
host: Environment.redisUrl,
host: Env.REDIS_URL,
},
},
);
+2 -2
View File
@@ -5,7 +5,7 @@ import fs from "fs/promises";
import type { GameGroup } from "../../../../common/src";
/* eslint-disable no-await-in-loop */
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import { asyncExec } from "#utils/misc";
import os from "os";
import path from "path";
@@ -189,7 +189,7 @@ export class DatabaseSeedsRepo {
* Pull any seeds changes in this repository.
*/
pull() {
if (Environment.nodeEnv === "dev") {
if (Env.NODE_ENV === "dev") {
// prevent an awful interaction where a user edits stuff on their disk
// and tries to run pnpm load-seeds
// but it fails because pull can't rebase with changes.
+142 -284
View File
@@ -1,326 +1,187 @@
import type { SendMailOptions } from "nodemailer";
import { FormatPrError } from "#utils/prudence";
import {
allSupportedGameGroups,
type integer,
type TachiServerCoreConfig,
} from "../../../../common/src";
// barrel file for re-exporting env variables.
import dotenv from "dotenv";
import fs from "fs";
import JSON5 from "json5";
import { p } from "prudence";
import { URL } from "url";
import { allImportTypes } from "../../../../common/src/constants/import-types";
// imports things like NODE_ENV from a local .env file if one is present.
dotenv.config();
import { allSupportedGameGroups, type GameGroup , type ImportTypes } from "tachi-common";
import { allImportTypes } from "tachi-common/constants/import-types";
import { z } from "zod";
// 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,
});
const rawConf = process.env.TCHIS_CONF;
if (!rawConf) {
logger.error("TCHIS_CONF environment variable is not set. Terminating.");
process.exit(1);
}
const config: unknown = JSON5.parse(confFile);
let config: unknown;
function isValidURL(self: unknown) {
if (typeof self !== "string") {
return `Expected URL, received type ${typeof self}`;
}
try {
new URL(self);
return true;
} catch (err) {
return `Invalid URL ${self} (${(err as Error).message}).`;
}
try {
config = JSON5.parse(rawConf);
} catch (err) {
logger.error("Failed to parse TCHIS_CONF as JSON5.", { err });
process.exit(1);
}
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;
MYT_API_HOST?: string;
CG_DEV_CONFIG?: CGConfig;
CG_NAG_CONFIG?: CGConfig;
CG_GAN_CONFIG?: CGConfig;
FLO_OAUTH2_INFO?: OAuth2Info;
EAG_OAUTH2_INFO?: OAuth2Info;
MIN_OAUTH2_INFO?: OAuth2Info;
ARC_AUTH_TOKEN?: string;
MYT_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;
ENABLE_METRICS: boolean;
SEEDS_CONFIG?:
| {
BRANCH?: string;
REPO_URL: string;
TYPE: "GIT_REPO";
USER_EMAIL: string | null;
USER_NAME: string | null;
}
| {
PATH: string;
TYPE: "LOCAL_FILES";
};
EMAIL_CONFIG?: {
DKIM?: SendMailOptions["dkim"];
FROM: string;
// @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;
BETA_USER_BONUS: integer;
INVITE_CAP: integer;
};
TACHI_CONFIG: TachiServerCoreConfig;
CDN_CONFIG: {
SAVE_LOCATION:
| {
ACCESS_KEY_ID: string;
BUCKET: string;
ENDPOINT: string;
KEY_PREFIX?: string;
REGION?: string;
SECRET_ACCESS_KEY: string;
TYPE: "S3_BUCKET";
}
| { LOCATION: string; SERVE_OWN_CDN?: boolean; TYPE: "LOCAL_FILESYSTEM" };
WEB_LOCATION: string;
};
}
const isValidOauth2 = p.optional({
CLIENT_ID: "string",
CLIENT_SECRET: "string",
REDIRECT_URI: "string",
const oauth2Schema = z.object({
CLIENT_ID: z.string(),
CLIENT_SECRET: z.string(),
REDIRECT_URI: z.string(),
});
const isValidCGConfig = p.optional({
API_KEY: "string",
URL: "string",
const cgConfigSchema = z.object({
API_KEY: z.string(),
URL: z.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),
MYT_API_HOST: "*string",
const configSchema = z.object({
MONGO_DATABASE_NAME: z.string(),
CAPTCHA_SECRET_KEY: z.string(),
SESSION_SECRET: z.string(),
FLO_API_URL: z.url().optional(),
EAG_API_URL: z.url().optional(),
MIN_API_URL: z.url().optional(),
ARC_API_URL: z.url().optional(),
MYT_API_HOST: z.string().optional(),
CG_DEV_CONFIG: isValidCGConfig,
CG_NAG_CONFIG: isValidCGConfig,
CG_GAN_CONFIG: isValidCGConfig,
CG_DEV_CONFIG: cgConfigSchema.optional(),
CG_NAG_CONFIG: cgConfigSchema.optional(),
CG_GAN_CONFIG: cgConfigSchema.optional(),
FLO_OAUTH2_INFO: isValidOauth2,
EAG_OAUTH2_INFO: isValidOauth2,
MIN_OAUTH2_INFO: isValidOauth2,
ARC_AUTH_TOKEN: "*string",
MYT_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",
ENABLE_METRICS: "*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",
FLO_OAUTH2_INFO: oauth2Schema.optional(),
EAG_OAUTH2_INFO: oauth2Schema.optional(),
MIN_OAUTH2_INFO: oauth2Schema.optional(),
ARC_AUTH_TOKEN: z.string().optional(),
MYT_AUTH_TOKEN: z.string().optional(),
ENABLE_SERVER_HTTPS: z.boolean().optional(),
CLIENT_DEV_SERVER: z.string().nullable().optional(),
RATE_LIMIT: z.number().int().positive().default(500),
OAUTH_CLIENT_CAP: z.number().int().positive().default(15),
OPTIONS_ALWAYS_SUCCEEDS: z.boolean().optional(),
USE_EXTERNAL_SCORE_IMPORT_WORKER: z.boolean().default(false),
EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY: z.number().int().positive().optional(),
ALLOW_RUNNING_OFFLINE: z.boolean().optional(),
ENABLE_METRICS: z.boolean().default(false),
EMAIL_CONFIG: z
.object({
FROM: z.string(),
// Nodemailer does not export the DKIM type properly, so we accept any object.
DKIM: z.any().optional(),
// The actual content is just a wacky options object — not worth asserting precisely.
TRANSPORT_OPS: z.any().optional(),
})
.optional(),
USC_QUEUE_SIZE: z.number().int().gte(2).default(3),
BEATORAJA_QUEUE_SIZE: z.number().int().gte(2).default(3),
MAX_GOAL_SUBSCRIPTIONS: z.number().int().positive().default(1_000),
MAX_QUEST_SUBSCRIPTIONS: z.number().int().positive().default(100),
MAX_FOLLOWING_AMOUNT: z.number().int().positive().default(1_000),
MAX_RIVALS: z.number().int().positive().default(5),
OUR_URL: z.string().refine((s) => !s.endsWith("/"), {
message: "OUR_URL must not end with a trailing slash.",
}),
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,
INVITE_CODE_CONFIG: z
.object({
BATCH_SIZE: z.number().int().positive(),
INVITE_CAP: z.number().int().positive(),
BETA_USER_BONUS: z.number().int().positive(),
})
.optional(),
TACHI_CONFIG: z.object({
NAME: z.string(),
TYPE: z.enum(["kamai", "boku", "omni"]),
GAMES: z.array(z.enum(allSupportedGameGroups as [GameGroup, ...GameGroup[]])),
IMPORT_TYPES: z.array(z.enum(allImportTypes as [ImportTypes, ...ImportTypes[]])),
SIGNUPS_ENABLED: z.boolean().default(true),
}),
TACHI_CONFIG: {
NAME: "string",
TYPE: p.isIn("kamai", "boku", "omni"),
GAMES: [p.isIn(allSupportedGameGroups)],
IMPORT_TYPES: [p.isIn(allImportTypes)],
SIGNUPS_ENABLED: p.optional("boolean"),
},
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(
p.or(
{
TYPE: p.is("GIT_REPO"),
REPO_URL: "string",
USER_NAME: "?string",
USER_EMAIL: "?string",
BRANCH: "*string",
},
{
TYPE: p.is("LOCAL_FILES"),
PATH: "string",
},
),
),
CDN_CONFIG: z.object({
WEB_LOCATION: z.string(),
SAVE_LOCATION: z.union([
z.object({
TYPE: z.literal("LOCAL_FILESYSTEM"),
SERVE_OWN_CDN: z.boolean().optional(),
LOCATION: z.string(),
}),
z.object({
TYPE: z.literal("S3_BUCKET"),
ENDPOINT: z.string(),
ACCESS_KEY_ID: z.string(),
SECRET_ACCESS_KEY: z.string(),
BUCKET: z.string(),
KEY_PREFIX: z.string().optional(),
REGION: z.string().optional(),
}),
]),
}),
SEEDS_CONFIG: z
.union([
z.object({
TYPE: z.literal("GIT_REPO"),
REPO_URL: z.string(),
USER_NAME: z.string().nullable(),
USER_EMAIL: z.string().nullable(),
BRANCH: z.string().optional(),
}),
z.object({
TYPE: z.literal("LOCAL_FILES"),
PATH: z.string(),
}),
])
.optional(),
});
if (err) {
throw new Error(FormatPrError(err, "Invalid conf.json5 file."));
export type OAuth2Info = z.infer<typeof oauth2Schema>;
export type CGConfig = z.infer<typeof cgConfigSchema>;
export type TachiServerConfig = z.infer<typeof configSchema>;
const result = configSchema.safeParse(config);
if (!result.success) {
throw new Error(`Invalid TCHIS_CONF: ${result.error.message}`);
}
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;
tachiServerConfig.TACHI_CONFIG.SIGNUPS_ENABLED ??= true;
tachiServerConfig.ENABLE_METRICS ??= false;
export const TachiConfig = tachiServerConfig.TACHI_CONFIG;
export const ServerConfig = tachiServerConfig;
export const TachiConfig = result.data.TACHI_CONFIG;
export const ServerConfig: TachiServerConfig = result.data;
// Environment Variable Validation
let port = Number(process.env.PORT);
let PORT = Number(process.env.PORT);
if (Number.isNaN(port) && process.env.IS_SERVER) {
if (Number.isNaN(PORT) && process.env.IS_SERVER) {
logger.warn(`No/invalid PORT specified in environment, defaulting to 8080.`);
port = 8080;
PORT = 8080;
}
const redisUrl = process.env.REDIS_URL ?? "";
const REDIS_URL = process.env.REDIS_URL;
if (!redisUrl) {
if (!REDIS_URL) {
// 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 ?? "";
const MONGO_URL = process.env.MONGO_URL;
if (!mongoUrl) {
if (!MONGO_URL) {
logger.error(`No MONGO_URL specified in environment. Terminating.`);
process.exit(1);
}
const nodeEnv = process.env.NODE_ENV ?? "";
const NODE_ENV = process.env.NODE_ENV;
if (!nodeEnv) {
if (!NODE_ENV) {
logger.error(`No NODE_ENV specified in environment. Terminating.`);
process.exit(1);
}
if (!["dev", "production", "staging", "test"].includes(nodeEnv)) {
if (!["dev", "production", "staging", "test"].includes(NODE_ENV)) {
logger.error(
`Invalid NODE_ENV set in environment. Expected dev, production, test or staging. Got ${nodeEnv}.`,
`Invalid NODE_ENV set in environment. Expected dev, production, test or staging. Got ${NODE_ENV}.`,
);
process.exit(1);
}
// if (bms XOR popn) is enabled
// if (bms XOR pms) 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.`,
@@ -337,24 +198,21 @@ if (!["crit", "debug", "error", "info", "severe", "verbose", "warn"].includes(lo
process.exit(1);
}
const replicaIdentity = process.env.REPLICA_IDENTITY;
const postgresUrl = process.env.POSTGRES_URL ?? "";
const POSTGRES_URL = process.env.POSTGRES_URL ?? "";
if (!postgresUrl) {
if (!POSTGRES_URL) {
logger.error(`No POSTGRES_URL specified in environment. Terminating.`);
process.exit(1);
}
export const Environment = {
port,
redisUrl,
mongoUrl,
postgresUrl,
nodeEnv: nodeEnv as "dev" | "production" | "staging" | "test",
replicaIdentity,
commitHash: process.env.COMMIT_HASH,
seqUrl: process.env.SEQ_URL,
seqApiKey: process.env.SEQ_API_KEY,
logLevel: logLevel as "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn",
// Typed variant of process.env
export const Env = {
PORT,
REDIS_URL,
MONGO_URL,
POSTGRES_URL,
NODE_ENV: NODE_ENV as "dev" | "production" | "staging" | "test",
COMMIT_HASH: process.env.COMMIT_HASH,
LOG_LEVEL: logLevel as "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn",
};
@@ -1,8 +1,5 @@
import { GPT_SERVER_IMPLEMENTATIONS } from "#game-implementations/game-implementations";
import {
type DerivedMetrics,
GAME_PT_CONFIGS,
GetGPTConfig,
type GPTString,
type OptionalMetrics,
+10 -14
View File
@@ -1,7 +1,3 @@
// Before we run anything, set a global to indicate to the code that
// we're running as a server, and not as a job runner or score worker.
process.env.IS_SERVER = "true";
import type http from "http";
import { LoadDefaultClients } from "#lib/builtin-clients/builtin-clients";
@@ -9,7 +5,7 @@ import { VERSION_PRETTY } from "#lib/constants/version";
import { HandleSIGTERMGracefully } from "#lib/handlers/sigterm";
import CreateLogCtx from "#lib/logger/logger";
import { ApplyUnappliedMigrations } from "#lib/migration/migrations";
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
import { AddNewUser } from "#server/router/api/v1/auth/auth";
import server from "#server/server";
import db, { monkDB } from "#services/mongo/db";
@@ -26,10 +22,10 @@ import { UserAuthLevels } from "../../common/src";
const logger = CreateLogCtx(__filename);
logger.info(`Booting ${TachiConfig.NAME} - ${VERSION_PRETTY} [ENV: ${Environment.nodeEnv}]`, {
logger.info(`Booting ${TachiConfig.NAME} - ${VERSION_PRETTY} [ENV: ${Env.NODE_ENV}]`, {
bootInfo: true,
});
logger.info(`Log level is set to ${Environment.logLevel}.`, { bootInfo: true });
logger.info(`Log level is set to ${Env.LOG_LEVEL}.`, { bootInfo: true });
logger.info(`Loading sequence documents...`, { bootInfo: true });
@@ -48,7 +44,7 @@ async function RunOnInit() {
}
});
if (Environment.nodeEnv === "dev") {
if (Env.NODE_ENV === "dev") {
const exists = await db.users.findOne({ id: 1 });
if (!exists) {
@@ -85,7 +81,7 @@ void RunOnInit();
let instance: http.Server | https.Server;
if (ServerConfig.ENABLE_SERVER_HTTPS === true) {
if (Environment.nodeEnv === "production") {
if (Env.NODE_ENV === "production") {
logger.warn(
"HTTPS Mode is enabled. This should not be used in production, and you should instead run behind a reverse proxy.",
{ bootInfo: true },
@@ -97,11 +93,11 @@ if (ServerConfig.ENABLE_SERVER_HTTPS === true) {
const httpsServer = https.createServer({ key: privateKey, cert: certificate }, server);
instance = httpsServer.listen(Environment.port);
logger.info(`HTTPS Listening on port ${Environment.port}`, { bootInfo: true });
instance = httpsServer.listen(Env.PORT);
logger.info(`HTTPS Listening on port ${Env.PORT}`, { bootInfo: true });
} else {
instance = server.listen(Environment.port);
logger.info(`HTTP Listening on port ${Environment.port}`, { bootInfo: true });
instance = server.listen(Env.PORT);
logger.info(`HTTP Listening on port ${Env.PORT}`, { bootInfo: true });
}
process.on("SIGTERM", () => {
@@ -111,7 +107,7 @@ process.on("SIGTERM", () => {
if (process.env.INVOKE_JOB_RUNNER) {
logger.info(`Spawning a tachi-server job runner inline.`, { bootInfo: true });
if (Environment.nodeEnv === "production") {
if (Env.NODE_ENV === "production") {
logger.warn(
`Spawning inline tachi-server job runner in production. This is bad for performance.`,
{ bootInfo: true },
@@ -129,7 +129,7 @@ interface InviteLockDocument {
const MONGO_URL = process.env.MONGO_URL ?? "mongodb://mongo/tachi";
const POSTGRES_URL = process.env.POSTGRES_URL;
const SEEDS_DIR = process.env.SEEDS_DIR ?? path.resolve(__dirname, "../../../seeds/collections");
const SEEDS_DIR = process.env.SEEDS_DIR ?? path.resolve(__dirname, "../../../../db/seeds");
if (!POSTGRES_URL) {
console.error("[migrate] POSTGRES_URL is not set.");
@@ -2,7 +2,7 @@ import type { Request } from "express";
import { ONE_MINUTE } from "#lib/constants/time";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
import { RedisClient } from "#services/redis/redis";
import { OmitUndefinedKeys } from "#utils/misc";
import rateLimit, { type Options } from "express-rate-limit";
@@ -15,7 +15,7 @@ const logger = CreateLogCtx(__filename);
function CreateStore(name: string) {
// undefined forces a default to an in-memory store
// So we use that when in testing or localdev.
return Environment.nodeEnv === "production" || Environment.nodeEnv === "staging"
return Env.NODE_ENV === "production" || Env.NODE_ENV === "staging"
? new RateLimitRedis({ prefix: `${TachiConfig.NAME}-RL:${name}`, client: RedisClient })
: undefined;
}
@@ -68,6 +68,6 @@ export const HyperAggressiveRateLimitMiddleware = rateLimit(
// vulnerable to bursting down the server.
// if we're in testing, disable this rate limit!
export const ScoreImportRateLimiter =
Environment.nodeEnv === "test"
Env.NODE_ENV === "test"
? rateLimit(CreateRateLimitOptions(Infinity, "ScImport", ONE_MINUTE))
: rateLimit(CreateRateLimitOptions(5, "ScImport", ONE_MINUTE));
@@ -1,6 +1,6 @@
import type { RequestHandler } from "express-serve-static-core";
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
/**
* Middleware that makes the route only available under Bokutachi.
@@ -56,7 +56,7 @@ export const RequireInvitesEnabled: RequestHandler = (req, res, next) => {
* at an expected checkout location.
*/
export const RequireLocalDevelopment: RequestHandler = (req, res, next) => {
if (Environment.nodeEnv === "dev" || Environment.nodeEnv === "test") {
if (Env.NODE_ENV === "dev" || Env.NODE_ENV === "test") {
next();
return;
}
@@ -1,6 +1,6 @@
import { ONE_MINUTE } from "#lib/constants/time";
import { ChangeRootLogLevel, GetLogLevel } from "#lib/logger/logger";
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import db from "#services/mongo/db";
import { CreateFakeAuthCookie } from "#test-utils/fake-auth";
import mockApi from "#test-utils/mock-api";
@@ -12,7 +12,7 @@ import t from "tap";
import { type ScoreDocument, UserAuthLevels } from "../../../../../../../common/src";
const LOG_LEVEL = Environment.logLevel;
const LOG_LEVEL = Env.LOG_LEVEL;
t.test("POST /api/v1/admin/change-log-level", async (t) => {
t.beforeEach(async () => {
@@ -5,7 +5,7 @@ import { SendSiteAnnouncementNotification } from "#lib/notifications/notificatio
import { UpdateGoalsForUser } from "#lib/score-import/framework/goals/goals";
import { UpdateQuestsForUser } from "#lib/score-import/framework/quests/quests";
import { DeleteMultipleScores, DeleteScore } from "#lib/score-mutation/delete-scores";
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
import prValidate from "#server/middleware/prudence-validate";
import db from "#services/mongo/db";
import { RecalcAllScores, UpdateAllPBs } from "#utils/calculations/recalc-scores";
@@ -60,7 +60,7 @@ const RequireAdminLevel: RequestHandler = async (req, res, next) => {
next();
};
const LOG_LEVEL = Environment.logLevel;
const LOG_LEVEL = Env.LOG_LEVEL;
router.use(RequireAdminLevel);
@@ -1,7 +1,7 @@
import type { PrivateUserInfoDocument } from "#utils/types";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import db from "#services/mongo/db";
import nodeFetch from "#utils/fetch";
import { Random20Hex } from "#utils/misc";
@@ -108,7 +108,7 @@ export async function AddNewUser(
};
// all created users on a dev instance should be admins, for convenience.
if (Environment.nodeEnv === "dev") {
if (Env.NODE_ENV === "dev") {
userDoc.authLevel = UserAuthLevels.ADMIN;
}
@@ -1,7 +1,7 @@
import { SendEmail } from "#lib/email/client";
import { EmailFormatResetPassword, EmailFormatVerifyEmail } from "#lib/email/formats";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
import prValidate from "#server/middleware/prudence-validate";
import {
AggressiveRateLimitMiddleware,
@@ -76,7 +76,7 @@ router.post(
logger.verbose(`Received login request with username ${body.username} (${req.ip})`);
/* istanbul ignore next */
if (Environment.nodeEnv === "production" || Environment.nodeEnv === "staging") {
if (Env.NODE_ENV === "production" || Env.NODE_ENV === "staging") {
logger.verbose("Validating captcha...");
const validCaptcha = await ValidateCaptcha(body.captcha, req.socket.remoteAddress);
@@ -214,7 +214,7 @@ router.post(
logger.verbose(`received register request with username ${body.username} (${req.ip})`);
/* istanbul ignore next */
if (Environment.nodeEnv === "production" || Environment.nodeEnv === "staging") {
if (Env.NODE_ENV === "production" || Env.NODE_ENV === "staging") {
logger.verbose("Validating captcha...");
const validCaptcha = await ValidateCaptcha(body.captcha, req.socket.remoteAddress);
@@ -463,7 +463,7 @@ router.post(
HyperAggressiveRateLimitMiddleware,
prValidate({ email: "string" }),
async (req, res) => {
if (!ServerConfig.EMAIL_CONFIG && Environment.nodeEnv !== "test") {
if (!ServerConfig.EMAIL_CONFIG && Env.NODE_ENV !== "test") {
return res.status(501).json({
success: false,
description: `This server does not support password resets.`,
@@ -1,6 +1,6 @@
import type { RequestHandler } from "express";
import { Environment } from "#lib/setup/config";
import { Env } from "#lib/setup/config";
import db from "#services/mongo/db";
import { AssignToReqTachiData, GetTachiData } from "#utils/req-tachi-data";
@@ -39,7 +39,7 @@ export const RequireOwnershipOfClient: RequestHandler = (req, res, next) => {
// There's an open issue for this here: https://github.com/i-like-robots/express-request-mock/issues/19
/* istanbul ignore next */
if (
Environment.nodeEnv === "test" &&
Env.NODE_ENV === "test" &&
(req.safeBody.__terribleHackOauth2ClientDoc as TachiAPIClientDocument | undefined)
) {
// obviously a glaring hack and security flaw - this only applies
@@ -1,6 +1,6 @@
import CreateLogCtx from "#lib/logger/logger";
import { PullDatabaseSeeds } from "#lib/seeds/repo";
import { Environment } from "#lib/setup/config";
import { Env } from "#lib/setup/config";
import prValidate from "#server/middleware/prudence-validate";
import { RequireLocalDevelopment } from "#server/middleware/type-require";
import { GetCommit, ListGitCommitsInPath } from "#utils/git";
@@ -37,9 +37,9 @@ const LOCAL_DEV_SEEDS_PATH = path.join(
);
const TEST_SEEDS_PATH = path.join(__dirname, "../../../../../test-utils/mock-db");
const LOCAL_SEEDS_PATH = Environment.nodeEnv === "test" ? TEST_SEEDS_PATH : LOCAL_DEV_SEEDS_PATH;
const LOCAL_SEEDS_PATH = Env.NODE_ENV === "test" ? TEST_SEEDS_PATH : LOCAL_DEV_SEEDS_PATH;
if (Environment.nodeEnv === "dev" || Environment.nodeEnv === "test") {
if (Env.NODE_ENV === "dev" || Env.NODE_ENV === "test") {
if (!fsSync.existsSync(LOCAL_SEEDS_PATH)) {
logger.error(
`Failed to load seeds routes, could not find any seeds/collections checked out at ${LOCAL_SEEDS_PATH}.
@@ -1,4 +1,4 @@
import { Environment } from "#lib/setup/config";
import { Env } from "#lib/setup/config";
import { UpdateLastSeen } from "#server/middleware/update-last-seen";
import { Router } from "express";
@@ -24,7 +24,7 @@ router.use("/api/v1", apiRouterV1);
// on the root endpoint.
// That is to say, if a user is hitting 127.0.0.1:8080/
// instead of "cannot GET /", they should get a nice message.
if (Environment.nodeEnv === "dev") {
if (Env.NODE_ENV === "dev") {
router.get("/", (req, res) =>
res.send(
`Server is live and running. All is good!<br />
+8 -8
View File
@@ -8,7 +8,7 @@ import type { integer } from "../../../common/src";
import { SYMBOL_TACHI_API_AUTH } from "#lib/constants/tachi";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
import { RedisClient } from "#services/redis/redis";
import { IsNonEmptyString, IsRecord } from "#utils/misc";
import ExpressPromBundle from "express-prom-bundle";
@@ -22,7 +22,7 @@ const logger = CreateLogCtx(__filename);
let store;
if (Environment.nodeEnv !== "test") {
if (Env.NODE_ENV !== "test") {
logger.info("Connecting ExpressSession to Redis.", { bootInfo: true });
const RedisStore = connectRedis(expressSession);
@@ -46,13 +46,13 @@ const userSessionMiddleware = expressSession({
// the absence of Secure in combination with SameSite=None will cause issues on non-https
// instances in newer versions of chromium. there is no workaround for this.
secure:
Environment.nodeEnv === "production" ||
Environment.nodeEnv === "staging" ||
Env.NODE_ENV === "production" ||
Env.NODE_ENV === "staging" ||
ServerConfig.ENABLE_SERVER_HTTPS,
// Very important. Without this, we're vulnerable to CSRF!
sameSite:
Environment.nodeEnv === "production" || Environment.nodeEnv === "staging"
Env.NODE_ENV === "production" || Env.NODE_ENV === "staging"
? "strict"
: "none",
},
@@ -60,7 +60,7 @@ const userSessionMiddleware = expressSession({
const app: Express = express();
if (Environment.nodeEnv !== "production" && IsNonEmptyString(ServerConfig.CLIENT_DEV_SERVER)) {
if (Env.NODE_ENV !== "production" && IsNonEmptyString(ServerConfig.CLIENT_DEV_SERVER)) {
logger.warn(`Enabling CORS requests from ${ServerConfig.CLIENT_DEV_SERVER}.`, {
bootInfo: true,
});
@@ -100,7 +100,7 @@ if (Environment.nodeEnv !== "production" && IsNonEmptyString(ServerConfig.CLIENT
app.options("*", (req, res) => res.send());
if (Environment.nodeEnv !== "test") {
if (Env.NODE_ENV !== "test") {
logger.info("Enabling Helmet, as no CLIENT_DEV_SERVER was set, or we are in production.", {
bootInfo: true,
});
@@ -159,7 +159,7 @@ if (
ServerConfig.CDN_CONFIG.SAVE_LOCATION.TYPE === "LOCAL_FILESYSTEM" &&
ServerConfig.CDN_CONFIG.SAVE_LOCATION.SERVE_OWN_CDN === true
) {
if (Environment.nodeEnv === "production") {
if (Env.NODE_ENV === "production") {
logger.warn(
`Running LOCAL_FILESYSTEM OWN_CDN in production. Consider making a separate process handle your CDN for performance.`,
{ bootInfo: true },
+5 -5
View File
@@ -50,7 +50,7 @@ import {
// so we just disable these rules. I know, it sucks, but we'll live.
import { ONE_MINUTE, ONE_SECOND } from "#lib/constants/time";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import { GetMillisecondsSince } from "#utils/misc";
const logger = CreateLogCtx(__filename);
@@ -58,21 +58,21 @@ const logger = CreateLogCtx(__filename);
let dbName = ServerConfig.MONGO_DATABASE_NAME;
/* istanbul ignore next */
if (Environment.nodeEnv === "test") {
if (Env.NODE_ENV === "test") {
dbName = `testingdb`;
}
logger.info(`Connecting to database ${Environment.mongoUrl}/${dbName}...`, { bootInfo: true });
logger.info(`Connecting to database ${Env.MONGO_URL}/${dbName}...`, { bootInfo: true });
const dbtime = process.hrtime.bigint();
export const monkDB = monk(`${Environment.mongoUrl}/${dbName}`, {
export const monkDB = monk(`${Env.MONGO_URL}/${dbName}`, {
// Various things cause bizarre issues with mongodb connections. Windows+Docker especially so.
// 5 minutes is excessive, but believe it or not, some setups are exceeding 2 minutes!
serverSelectionTimeoutMS: ONE_MINUTE * 5,
// in local dev, don't **ever** add _id onto objects you're inserting
// in production, this might have a performance hit.
forceServerObjectId: Environment.nodeEnv === "test",
forceServerObjectId: Env.NODE_ENV === "test",
});
/* istanbul ignore next */
+2 -2
View File
@@ -1,10 +1,10 @@
import type { Database } from "tachi-db";
import { Environment } from "#lib/setup/config";
import { Env } from "#lib/setup/config";
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
const pool = new Pool({ connectionString: Environment.postgresUrl });
const pool = new Pool({ connectionString: Env.POSTGRES_URL });
const pgDb = new Kysely<Database>({
dialect: new PostgresDialect({ pool }),
@@ -1,6 +1,6 @@
import { ONE_MINUTE, ONE_SECOND } from "#lib/constants/time";
import CreateLogCtx from "#lib/logger/logger";
import { Environment } from "#lib/setup/config";
import { Env } from "#lib/setup/config";
import { GetMillisecondsSince } from "#utils/misc";
import redis from "redis";
@@ -9,7 +9,7 @@ const logger = CreateLogCtx(__filename);
logger.verbose("Instantiated Redis Store", { bootInfo: true });
export const RedisClient = redis.createClient({
url: `redis://${Environment.redisUrl}`,
url: `redis://${Env.REDIS_URL}`,
});
const startConnect = process.hrtime.bigint();
+2 -2
View File
@@ -1,7 +1,7 @@
import type { ICollection } from "monk";
import CreateLogCtx from "#lib/logger/logger";
import { Environment, ServerConfig } from "#lib/setup/config";
import { Env, ServerConfig } from "#lib/setup/config";
import { ClearTestingRateLimitCache } from "#server/middleware/rate-limiter";
import db, { type StaticDatabases } from "#services/mongo/db";
import { SetIndexes } from "#services/mongo/indexes";
@@ -116,7 +116,7 @@ export function ResetCDN() {
export async function SetIndexesForDB() {
await ResetDBState();
const url = `${Environment.mongoUrl}/testingdb`;
const url = `${Env.MONGO_URL}/testingdb`;
logger.info(`Setting indexes for ${url}`);
+2 -2
View File
@@ -1,8 +1,8 @@
import { Environment } from "#lib/setup/config";
import { Env } from "#lib/setup/config";
import nodeFetch, { type RequestInfo, type RequestInit, type Response } from "node-fetch";
const fetch =
Environment.nodeEnv === "test"
Env.NODE_ENV === "test"
? () => {
throw new Error("Cannot use real fetch inside testing env!");
}
+4 -1
View File
@@ -3,7 +3,10 @@
"compilerOptions": {
"module": "Preserve",
"moduleResolution": "bundler",
"lib": ["ES2022"]
"lib": ["ES2022"],
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
+4 -1
View File
@@ -2,7 +2,10 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "Preserve",
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"paths": {
"#*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]