import path from "node:path"; const env = process.env; /** * parseInt with a default that distinguishes "unset" from "explicit 0". * * `min` clamps the result. Pass it for any value where 0 is not a coherent * setting — a pool of 0 workers, 0 concurrent jobs or a 0-second timeout * wedges or crashes the feature rather than disabling it. Values that fail * closed at 0 (byte caps) deliberately have no minimum. */ function intEnv( value: string | undefined, defaultValue: number, min = Number.NEGATIVE_INFINITY, ): number { if (value === undefined || value === "") return defaultValue; const parsed = parseInt(value, 10); return Number.isFinite(parsed) ? Math.max(min, parsed) : defaultValue; } const PORT = intEnv(env.PORT, 3000, 1); const config = { OWNER_DISPLAY_NAME: env.OWNER_DISPLAY_NAME ?? "Admin", INLINE_MAX_BYTES: intEnv(env.INLINE_MAX_BYTES, 524288), MAX_UPLOAD_BYTES: intEnv(env.MAX_UPLOAD_BYTES, 10 * 1024 * 1024), MAX_USER_UPLOAD_BYTES: intEnv(env.MAX_USER_UPLOAD_BYTES, 2 * 1024 * 1024), TRUSTED_PROXY: !!env.TRUSTED_PROXY, RATE_LIMIT_DISABLED: !!env.RATE_LIMIT_DISABLED, SSH_DISABLED: !!env.SSH_DISABLED, SCANNED_REPO_PRIVATE: env.SCANNED_REPO_PRIVATE !== "0" && env.SCANNED_REPO_PRIVATE !== "false", PORT, SSH_PORT: intEnv(env.SSH_PORT, 2222, 1), REGISTRATION_TYPE: (env.REGISTRATION_TYPE ?? "enabled") as | "enabled" | "disabled" | "queue", REGISTER_QUESTION: env.REGISTER_QUESTION ?? "", BASE_URL: env.BASE_URL ?? `http://localhost:${PORT}`, // Derived from BASE_URL after the literal closes (see below). Declared // here so consumers get a typed config object. PUBLIC_HTTPS: false, PUBLIC_ORIGIN: "", DATA_DIR: path.resolve(env.DATA_DIR ?? "./data"), HIGHLIGHT_WORKERS: intEnv(env.HIGHLIGHT_WORKERS, 4, 1), COMMITTER_NAME: env.COMMITTER_NAME ?? env.OWNER_DISPLAY_NAME ?? "Admin", COMMITTER_EMAIL: "", EXTRA_ALLOWED_SIGNERS_PATH: env.EXTRA_ALLOWED_SIGNERS_PATH ?? null, MAX_TITLE_BYTES: intEnv(env.MAX_TITLE_BYTES, 500), MAX_TEXT_BODY_BYTES: intEnv(env.MAX_TEXT_BODY_BYTES, 100_000), MAX_USERNAME_BYTES: intEnv(env.MAX_USERNAME_BYTES, 64), MAX_PASSWORD_BYTES: intEnv(env.MAX_PASSWORD_BYTES, 1024), CI_DOCKER_SOCKET: env.CI_DOCKER_SOCKET ?? "", CI_MAX_HISTORY: intEnv(env.CI_MAX_HISTORY, 50), CI_MAX_CONCURRENT: intEnv(env.CI_MAX_CONCURRENT, 2, 1), CI_DEFAULT_TIMEOUT: intEnv(env.CI_DEFAULT_TIMEOUT, 3600, 1), // Cap on simultaneous source archive generation jobs (one release with // include_source_code spawns three git-archive + compressor pipelines // back-to-back). Without this cap, an admin firing several releases in // quick succession can saturate CPU. Excess jobs are queued in memory. MAX_CONCURRENT_ARCHIVE_JOBS: intEnv(env.MAX_CONCURRENT_ARCHIVE_JOBS, 2, 1), // Cap on any server-side render that holds the whole content in // RAM and runs synchronous CPU work on it (blob view, commit/patch // diff, markdown). Above this, the UI shows a "too large to // preview" stub and links to the raw endpoint. Setting to 0 // disables inline rendering entirely. MAX_RENDER_BYTES: intEnv(env.MAX_RENDER_BYTES, 10 * 1024 * 1024), // Cap on the streamed /raw download. 0 means no limit (current // behaviour) — useful on a trusted LAN where you actually want to // pull large blobs out of the browser. Public deployments should // pin this to something sane. MAX_RAW_DOWNLOAD_BYTES: intEnv(env.MAX_RAW_DOWNLOAD_BYTES, 0), }; // Derived values that depend on other config fields const baseUrl = new URL(config.BASE_URL); config.PUBLIC_HTTPS = baseUrl.protocol === "https:"; config.PUBLIC_ORIGIN = baseUrl.origin; config.COMMITTER_EMAIL = env.COMMITTER_EMAIL ?? `${config.OWNER_DISPLAY_NAME}@${baseUrl.hostname}`; export default config;