/** * Typed environment for the API. Values come from process.env; in development the repo-root `.env` * is loaded through node's `process.loadEnvFile` (never overriding already-set variables). */ import { existsSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import process from "node:process"; export interface ApiEnv { port: number; host: string; databaseUrl: string; redisUrl: string; clickhouseUrl: string; clickhouseDb: string; s3Endpoint: string; s3Bucket: string; s3AccessKey: string; s3SecretKey: string; s3Region: string; /** null when DCI_ADMIN_TOKEN is missing, empty or the "change-me" placeholder → admin API answers 503 */ adminToken: string | null; /** internal HTTP server of the worker (GET /trace/, GET /data-gaps) */ workerUrl: string; /** directory holding connector YAML files */ configDir: string; /** optional path to the worker connectors registry (parsers) for the dev tool; null = generic only */ workerConnectorsDir: string | null; logLevel: string; cacheEnabled: boolean; publicRateLimit: number; adminRateLimit: number; trustedProxies: string[]; siteUrl: string; } export function findRepoRoot(start: string = process.cwd()): string { let dir = resolve(start); for (let i = 0; i < 8; i++) { if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir; const parent = dirname(dir); if (parent === dir) break; dir = parent; } const here = dirname(fileURLToPath(import.meta.url)); return resolve(here, "..", "..", ".."); } let envFileLoaded = false; export function loadEnvFile(file = ".env"): boolean { if (envFileLoaded) return true; const path = file.startsWith("/") ? file : join(findRepoRoot(), file); if (!existsSync(path)) return false; const loader = (process as unknown as { loadEnvFile?: (p: string) => void }).loadEnvFile; if (typeof loader !== "function") return false; loader.call(process, path); envFileLoaded = true; return true; } function num(v: string | undefined, fallback: number): number { const n = Number(v); return v !== undefined && v !== "" && Number.isFinite(n) ? n : fallback; } let cached: ApiEnv | null = null; export function getEnv(): ApiEnv { if (cached) return cached; const e = process.env; const root = findRepoRoot(); const workerDir = e.DCI_WORKER_CONNECTORS_DIR ?? join(root, "apps", "worker", "src", "connectors"); cached = { port: num(e.API_PORT, 8311), host: e.API_HOST ?? "0.0.0.0", databaseUrl: e.DATABASE_URL ?? "postgres://dci:dci@127.0.0.1:5432/dci", redisUrl: e.REDIS_URL ?? "redis://127.0.0.1:6379/0", clickhouseUrl: e.CLICKHOUSE_URL ?? "http://127.0.0.1:8123", clickhouseDb: e.CLICKHOUSE_DB ?? "dci", s3Endpoint: e.S3_ENDPOINT ?? "http://127.0.0.1:9000", s3Bucket: e.S3_BUCKET ?? "dci-raw", s3AccessKey: e.S3_ACCESS_KEY ?? "dci", s3SecretKey: e.S3_SECRET_KEY ?? "", s3Region: e.S3_REGION ?? "us-east-1", adminToken: e.DCI_ADMIN_TOKEN && e.DCI_ADMIN_TOKEN.trim() !== "" && e.DCI_ADMIN_TOKEN !== "change-me" ? e.DCI_ADMIN_TOKEN : null, workerUrl: (e.DCI_WORKER_URL && e.DCI_WORKER_URL.trim()) || "http://127.0.0.1:8320", configDir: e.DCI_CONFIG_DIR ? resolve(e.DCI_CONFIG_DIR) : join(root, "config", "connectors"), workerConnectorsDir: existsSync(workerDir) ? workerDir : null, logLevel: (e.DCI_LOG_LEVEL ?? "info").toLowerCase(), cacheEnabled: e.DCI_API_CACHE !== "0", publicRateLimit: num(e.DCI_API_RATE_LIMIT, 240), adminRateLimit: num(e.DCI_ADMIN_RATE_LIMIT, 60), trustedProxies: (e.DCI_TRUSTED_PROXIES ?? "127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,::1/128").split(",").map((s) => s.trim()).filter(Boolean), siteUrl: e.NEXT_PUBLIC_SITE_URL ?? "https://www.datacenterindex.io", }; return cached; } /** Test hook: forget the cached env (after mutating process.env). */ export function resetEnv(): void { cached = null; }