config.ts
Raw
1import path from "node:path";
2
3const env = process.env;
4
5/** parseInt with a default that distinguishes "unset" from "explicit 0". */
6function intEnv(value: string | undefined, defaultValue: number): number {
7 if (value === undefined || value === "") return defaultValue;
8 const parsed = parseInt(value, 10);
9 return Number.isFinite(parsed) ? parsed : defaultValue;
10}
11
12const config = {
13 OWNER_DISPLAY_NAME: env.OWNER_DISPLAY_NAME ?? "Admin",
14 INLINE_MAX_BYTES: parseInt(env.INLINE_MAX_BYTES ?? "", 10) || 524288,
15 MAX_UPLOAD_BYTES:
16 parseInt(env.MAX_UPLOAD_BYTES ?? "", 10) || 10 * 1024 * 1024,
17 MAX_USER_UPLOAD_BYTES:
18 parseInt(env.MAX_USER_UPLOAD_BYTES ?? "", 10) || 2 * 1024 * 1024,
19 TRUSTED_PROXY: !!env.TRUSTED_PROXY,
20 RATE_LIMIT_DISABLED: !!env.RATE_LIMIT_DISABLED,
21 SSH_DISABLED: !!env.SSH_DISABLED,
22 SCANNED_REPO_PRIVATE:
23 env.SCANNED_REPO_PRIVATE !== "0" &&
24 env.SCANNED_REPO_PRIVATE !== "false",
25 PORT: parseInt(env.PORT ?? "", 10) || 3000,
26 SSH_PORT: parseInt(env.SSH_PORT ?? "", 10) || 2222,
27 REGISTRATION_TYPE: (env.REGISTRATION_TYPE ?? "enabled") as
28 | "enabled"
29 | "disabled"
30 | "queue",
31 REGISTER_QUESTION: env.REGISTER_QUESTION ?? "",
32 BASE_URL:
33 env.BASE_URL ??
34 `http://localhost:${parseInt(env.PORT ?? "", 10) || 3000}`,
35 // Derived from BASE_URL after the literal closes (see below). Declared
36 // here so consumers get a typed config object.
37 PUBLIC_HTTPS: false,
38 PUBLIC_ORIGIN: "",
39 DATA_DIR: path.resolve(env.DATA_DIR ?? "./data"),
40 HIGHLIGHT_WORKERS: parseInt(env.HIGHLIGHT_WORKERS ?? "", 10) || 4,
41 COMMITTER_NAME: env.COMMITTER_NAME ?? env.OWNER_DISPLAY_NAME ?? "Admin",
42 COMMITTER_EMAIL: "",
43 EXTRA_ALLOWED_SIGNERS_PATH: env.EXTRA_ALLOWED_SIGNERS_PATH ?? null,
44 MAX_TITLE_BYTES: parseInt(env.MAX_TITLE_BYTES ?? "", 10) || 500,
45 MAX_TEXT_BODY_BYTES: parseInt(env.MAX_TEXT_BODY_BYTES ?? "", 10) || 100_000,
46 MAX_USERNAME_BYTES: parseInt(env.MAX_USERNAME_BYTES ?? "", 10) || 64,
47 MAX_PASSWORD_BYTES: parseInt(env.MAX_PASSWORD_BYTES ?? "", 10) || 1024,
48 CI_DOCKER_SOCKET: env.CI_DOCKER_SOCKET ?? "",
49 CI_MAX_HISTORY: parseInt(env.CI_MAX_HISTORY ?? "", 10) || 50,
50 CI_MAX_CONCURRENT: parseInt(env.CI_MAX_CONCURRENT ?? "", 10) || 2,
51 CI_DEFAULT_TIMEOUT: parseInt(env.CI_DEFAULT_TIMEOUT ?? "", 10) || 3600,
52 // Cap on simultaneous source archive generation jobs (one release with
53 // include_source_code spawns three git-archive + compressor pipelines
54 // back-to-back). Without this cap, an admin firing several releases in
55 // quick succession can saturate CPU. Excess jobs are queued in memory.
56 MAX_CONCURRENT_ARCHIVE_JOBS:
57 parseInt(env.MAX_CONCURRENT_ARCHIVE_JOBS ?? "", 10) || 2,
58 // Cap on any server-side render that holds the whole content in
59 // RAM and runs synchronous CPU work on it (blob view, commit/patch
60 // diff, markdown). Above this, the UI shows a "too large to
61 // preview" stub and links to the raw endpoint. Setting to 0
62 // disables inline rendering entirely.
63 MAX_RENDER_BYTES: intEnv(env.MAX_RENDER_BYTES, 10 * 1024 * 1024),
64 // Cap on the streamed /raw download. 0 means no limit (current
65 // behaviour) — useful on a trusted LAN where you actually want to
66 // pull large blobs out of the browser. Public deployments should
67 // pin this to something sane.
68 MAX_RAW_DOWNLOAD_BYTES: intEnv(env.MAX_RAW_DOWNLOAD_BYTES, 0),
69};
70
71// Derived values that depend on other config fields
72const baseUrl = new URL(config.BASE_URL);
73config.PUBLIC_HTTPS = baseUrl.protocol === "https:";
74config.PUBLIC_ORIGIN = baseUrl.origin;
75config.COMMITTER_EMAIL =
76 env.COMMITTER_EMAIL ?? `${config.OWNER_DISPLAY_NAME}@${baseUrl.hostname}`;
77
78export default config;
79