import Dict = NodeJS.Dict;
const DEFINITION = `CoCalc Environment Variables:
- root -- if COCALC_ROOT is set then it; otherwise use [cocalc-source]/src/.
- data -- if the environment variable DATA is set, use that. Otherwise, use {root}/data
- pgdata -- if env var PGDATA is set, use that; otherwise, it is {data}/postgres: where data data is stored (if running locally)
- pghost - if env var PGHOST is set, use that; otherwise, it is {data}/postgres/socket: what database connects to
- projects -- If env var PROJECTS is set, use that; otherwise, it is {data}"/projects/[project_id]";
This is where project home directories are (or shared files for share server), and it MUST
contain the string "[project_id]".
- secrets -- if env var SECRETS is set, use that; otherwise, it is {data}/secrets: where to store secrets
- logs -- if env var LOGS is set, use that; otherwise, {data}/logs: directory in which to store logs
`;
import { join, resolve } from "path";
import { ConnectionOptions } from "node:tls";
import { existsSync, mkdirSync, readFileSync } from "fs";
import { isEmpty } from "lodash";
import basePath from "@cocalc/backend/base-path";
import port from "@cocalc/backend/port";
function determineRootFromPath(): string {
const cur = __dirname;
const search = "/src/";
const i = cur.lastIndexOf(search);
const root = resolve(cur.slice(0, i + search.length - 1));
process.env.COCALC_ROOT = root;
return root;
}
interface CoCalcSSLEnvConfig extends Dict<string> {
SMC_DB_SSL?: string;
SMC_DB_SSL_CA_FILE?: string;
SMC_DB_SSL_CLIENT_CERT_FILE?: string;
SMC_DB_SSL_CLIENT_KEY_FILE?: string;
SMC_DB_SSL_CLIENT_KEY_PASSPHRASE?: string;
}
export interface PsqlSSLEnvConfig {
PGSSLMODE?: "verify-full" | "require";
PGSSLROOTCERT?: "system" | string;
PGSSLCERT?: string;
PGSSLKEY?: string;
}
export type SSLConfig =
| (ConnectionOptions & {
caFile?: string;
clientCertFile?: string;
clientKeyFile?: string;
})
| boolean
| undefined;
export function sslConfigFromCoCalcEnv(
env: CoCalcSSLEnvConfig = process.env,
): SSLConfig {
const sslConfig: SSLConfig = {};
if (env.SMC_DB_SSL_CA_FILE) {
sslConfig.caFile = env.SMC_DB_SSL_CA_FILE;
sslConfig.ca = readFileSync(env.SMC_DB_SSL_CA_FILE);
}
if (env.SMC_DB_SSL_CLIENT_CERT_FILE) {
sslConfig.clientCertFile = env.SMC_DB_SSL_CLIENT_CERT_FILE;
sslConfig.cert = readFileSync(env.SMC_DB_SSL_CLIENT_CERT_FILE);
}
if (env.SMC_DB_SSL_CLIENT_KEY_FILE) {
sslConfig.clientKeyFile = env.SMC_DB_SSL_CLIENT_KEY_FILE;
sslConfig.key = readFileSync(env.SMC_DB_SSL_CLIENT_KEY_FILE);
}
if (env.SMC_DB_SSL_CLIENT_KEY_PASSPHRASE) {
sslConfig.passphrase = env.SMC_DB_SSL_CLIENT_KEY_PASSPHRASE;
}
return isEmpty(sslConfig)
? env.SMC_DB_SSL?.toLowerCase() === "true"
: sslConfig;
}
export function sslConfigToPsqlEnv(config: SSLConfig): PsqlSSLEnvConfig {
if (!config) {
return {};
} else if (config === true) {
return {
PGSSLMODE: "require",
};
}
const psqlArgs: PsqlSSLEnvConfig = {
PGSSLMODE: "verify-full",
};
if (config.caFile) {
psqlArgs.PGSSLROOTCERT = `${config.caFile}`;
} else {
psqlArgs.PGSSLROOTCERT = "system";
}
if (config.clientCertFile) {
psqlArgs.PGSSLCERT = `${config.clientCertFile}`;
}
if (config.clientKeyFile) {
psqlArgs.PGSSLKEY = `${config.clientKeyFile}`;
}
return psqlArgs;
}
export const root: string = process.env.COCALC_ROOT ?? determineRootFromPath();
export const data: string = process.env.DATA ?? join(root, "data");
export const pguser: string = process.env.PGUSER ?? "smc";
export const pgdata: string = process.env.PGDATA ?? join(data, "postgres");
export const pghost: string = process.env.PGHOST ?? join(pgdata, "socket");
export const pgssl = sslConfigFromCoCalcEnv();
export const pgdatabase: string =
process.env.SMC_DB ?? process.env.PGDATABASE ?? "smc";
export const projects: string =
process.env.PROJECTS ?? join(data, "projects", "[project_id]");
export const secrets: string = process.env.SECRETS ?? join(data, "secrets");
export const syncFiles = {
local: process.env.COCALC_SYNC ?? join(data, "sync"),
archive: process.env.COCALC_SYNC_ARCHIVE ?? "",
};
if (!existsSync(secrets)) {
try {
mkdirSync(secrets, { recursive: true, mode: 0o700 });
} catch {
}
}
export const logs: string = process.env.LOGS ?? join(data, "logs");
export let conatServer =
process.env.CONAT_SERVER ??
`http://localhost:${port}${basePath.length > 1 ? basePath : ""}`;
if (conatServer.split("//").length > 2) {
throw Error(
`env variable CONAT_SERVER invalid -- too many /s' --'${process.env.CONAT_SERVER}'`,
);
}
export function setConatServer(server: string) {
conatServer = server;
}
export let conatPassword = "";
export const conatPasswordPath = join(secrets, "conat-password");
try {
conatPassword = readFileSync(conatPasswordPath).toString().trim();
} catch {}
export function setConatPassword(password: string) {
conatPassword = password;
}
export let conatValkey: string = process.env.CONAT_VALKEY ?? "";
export function setConatValkey(valkey: string) {
conatValkey = valkey;
}
export let valkeyPassword = "";
const valkeyPasswordPath = join(secrets, "valkey-password");
try {
valkeyPassword = readFileSync(valkeyPasswordPath).toString().trim();
} catch {}
export let conatSocketioCount = parseInt(
process.env.CONAT_SOCKETIO_COUNT ?? "1",
);
export let conatPersistCount = parseInt(process.env.CONAT_PERSIST_COUNT ?? "1");
export let conatApiCount = parseInt(process.env.CONAT_API_COUNT ?? "1");
export let conatClusterPort = parseInt(process.env.CONAT_CLUSTER_PORT ?? "0");
export let conatClusterHealthPort = parseInt(
process.env.CONAT_CLUSTER_HEALTH_PORT ?? "0",
);
export let apiKey: string = process.env.API_KEY ?? "";
export let apiServer: string = process.env.API_SERVER ?? "";
delete process.env.API_KEY;
export function setApi({ key, server }: { key?: string; server?: string }) {
if (key != null) {
apiKey = key;
}
if (server != null) {
checkApiServer(server);
apiServer = server;
}
}
function sanityChecks() {
if (!projects.includes("[project_id]")) {
throw Error(
`${DEFINITION}\n\nenv variable PROJECTS must contain "[project_id]" but it is "${process.env.PROJECTS}"`,
);
}
checkApiServer(apiServer);
}
function checkApiServer(server) {
if (!server) return;
if (server.endsWith("/")) {
throw Error("API_SERVER must not end in /");
}
if (!server.startsWith("http://") && !server.startsWith("https://")) {
throw Error("API_SERVER must start with http:// or https://");
}
}
sanityChecks();