import { readFileSync } from "node:fs"; import { type InferOutput, safeParse } from "@atcute/lexicons"; import { CatVt3eBbellServerDefs } from "@vt3e.cat/bbell-lexicons"; import { z } from "zod"; import { version } from "../package.json"; import { loggers } from "./logger"; const logger = loggers.config; const env = Bun.env.NODE_ENV || "development"; const filePath = `.config.${env}.json`; type ParseFailed = { ok: false; message?: string; issues: { path: (string | number)[]; message: string }[]; }; type ParseSuccess = { ok: true; value: T; }; type ParseResult = ParseSuccess | ParseFailed; const runtimeSchema = z.object({ port: z.number().int().min(1).max(65535).default(3000), dbFile: z.string().min(1), secrets: z.object({ cookieSecret: z.string().min(8), jwtSecret: z.string().min(16), privateKeyJwk: z.record(z.string(), z.unknown()), }), }); function getConfigJson() { let raw: string; try { raw = readFileSync(filePath, "utf8"); } catch (err) { logger.error( `failed to read config file ${filePath}: ${(err as Error).message}`, ); process.exit(1); } let json: Record; try { json = JSON.parse(raw); } catch (err) { logger.error( `failed to parse JSON in ${filePath}: ${(err as Error).message}`, ); process.exit(1); } const requiredObjects = ["runtime", "service"]; const issues: string[] = []; for (const key of requiredObjects) { if (typeof json[key] !== "object" || json[key] === null) { issues.push(`'${key}' must be an object`); } } if (issues.length) { logger.error(`config validation failed:`); for (const issue of issues) logger.error(` ${issue}`); process.exit(1); } return json as { runtime: Record; service: Record; }; } function logFailure(target: string, error: ParseFailed) { logger.error(`failed to parse ${target}`); let indentation = " "; if (error.message) { logger.error(` ${error.message}`); indentation += " "; } for (const issue of error.issues) logger.error(`${indentation}${issue.path.join(".")}: ${issue.message}`); logger.error(""); } function parseRuntimeConfig( input: Record, ): ParseResult> { const runtimeParse = runtimeSchema.safeParse(input); if (!runtimeParse.success) { return { ok: false, issues: runtimeParse.error.issues.map((issue) => ({ path: issue.path.map(String), message: issue.message, })), }; } const port = Number(process.env.PORT ?? "3000"); return { ok: true, value: { ...runtimeParse.data, port } }; } function parseServiceConfig( input: Record, ): ParseResult> { const serviceParse = safeParse(CatVt3eBbellServerDefs.configSchema, input); if (!serviceParse.ok) { return { ok: false, message: serviceParse.message, issues: serviceParse.issues.map((issue) => ({ path: issue.path, message: issue.code, })), }; } return { ok: true, value: serviceParse.value, }; } function getConfig() { const config = getConfigJson(); const { runtime: _runtimeConfig, service: _serviceConfig } = config; const runtimeConfig = parseRuntimeConfig(_runtimeConfig); const serviceConfig = parseServiceConfig({ ..._serviceConfig, version }); if (!runtimeConfig.ok) logFailure("runtime config", runtimeConfig); if (!serviceConfig.ok) logFailure("service config", serviceConfig); if (!runtimeConfig.ok || !serviceConfig.ok) process.exit(1); return { runtime: { ...runtimeConfig.value, dbFile: runtimeConfig.value.dbFile.replace( "{env}", Bun.env.NODE_ENV || "development", ), env: Bun.env.NODE_ENV || "development", isProduction: (Bun.env.NODE_ENV || "development") === "production", }, service: serviceConfig.value, }; } const config = getConfig(); const runtime = config.runtime; const service = config.service; export default config.runtime; export { runtime, service };