Files
factorio-clusterio/packages/lib/src/config/definitions.ts
T
Hornwitser fb3fe09466 Remove tlsCa option
The migration from phin to fetch in #755 was hampred by the lack of an
option to set the CA when using fetch.  This meant places that used phin
to talk to the controller with the tlsCa option could not be migrated to
use fetch.

Remove the tlsCa option to unblock this dependency. The replacement for
this option is to set the NODE_EXTRA_CA_CERTS environment variable to a
path to the certificate file instead, which works with fetch.
2026-03-04 17:43:32 +01:00

699 lines
22 KiB
TypeScript

// Core definitions for the configuration system
import * as classes from "./classes";
import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin";
import { Static } from "@sinclair/typebox";
type configFromJSON<T> = (...args: Parameters<typeof classes.Config.fromJSON>) => T;
type configFromFile<T> = (...args: Parameters<typeof classes.Config.fromFile>) => Promise<T>;
export interface ControllerConfigFields {
"controller.version": string;
"controller.name": string;
"controller.mods_directory": string;
"controller.database_directory": string;
"controller.http_port": number | null;
"controller.https_port": number | null;
"controller.bind_address": string | null;
"controller.trusted_proxies": string | null;
"controller.public_url": string | null;
"controller.tls_certificate": string | null;
"controller.tls_private_key": string | null;
"controller.auth_secret": string;
"controller.heartbeat_interval": number;
"controller.session_timeout": number;
"controller.metrics_timeout": number;
"controller.system_metrics_interval": number;
"controller.proxy_stream_timeout": number;
"controller.factorio_username": string | null;
"controller.factorio_token": string | null;
"controller.share_factorio_credentials_with_hosts": boolean;
"controller.default_mod_pack_id": number | null;
"controller.default_role_id": number | null;
"controller.autosave_interval": number;
"controller.mod_portal_cache_duration_minutes": number;
"controller.mod_portal_page_size": number;
"controller.allow_remote_updates": boolean;
"controller.allow_plugin_updates": boolean;
"controller.allow_plugin_install": boolean;
[key: `${string}.load_plugin`]: boolean;
}
/**
* Controller Config class
* @extends classes.Config
*/
export class ControllerConfig extends classes.Config<ControllerConfigFields> {
declare static fromJSON: configFromJSON<ControllerConfig>;
declare static fromFile: configFromFile<ControllerConfig>;
static migrations(config: Static<typeof this.jsonSchema>) {
if (config.hasOwnProperty("controller.external_address")) {
config["controller.public_url"] = config["controller.external_address"];
delete config["controller.external_address"];
}
return config;
}
static fieldDefinitions: classes.ConfigDefs<ControllerConfigFields> = {
"controller.version": {
type: "string",
initialValue: "", // Set on start
readonly: ["controller"],
hidden: true,
},
"controller.name": {
title: "Name",
description: "Name of the cluster.",
type: "string",
initialValue: "Your Cluster",
},
"controller.mods_directory": {
title: "Mods Directory",
description: "Path to directory where mods shared with the cluster are stored.",
restartRequired: true,
type: "string",
initialValue: "mods",
},
"controller.database_directory": {
title: "Database directory",
description: "Directory where item and configuration data is stored.",
type: "string",
initialValue: "database",
},
"controller.autosave_interval": {
title: "Autosave Interval",
description: "Interval in seconds to autosave data in memory to disk.",
type: "number",
initialValue: 60,
},
"controller.http_port": {
title: "HTTP Port",
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
restartRequired: true,
type: "number",
optional: true,
initialValue: 8080,
},
"controller.https_port": {
title: "HTTPS Port",
description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.",
restartRequired: true,
type: "number",
optional: true,
},
"controller.bind_address": {
title: "Bind Address",
description: "IP address to bind the HTTP and HTTPS ports on.",
restartRequired: true,
type: "string",
optional: true,
},
"controller.trusted_proxies": {
title: "Trusted Proxies",
description:
"Comma separated list of IP addresses and/or CIDR blocks to trust the X-Forwarded-For header on",
type: "string",
optional: true,
},
"controller.public_url": {
title: "Public URL",
description: "Public facing URL the controller is hosted on, including the protocol.",
type: "string",
optional: true,
},
"controller.tls_certificate": {
title: "TLS Certificate",
description: "Path to the certificate to use for HTTPS.",
restartRequired: true,
type: "string",
optional: true,
},
"controller.tls_private_key": {
title: "TLS Private Key",
description: "Path to the private key to use for HTTPS.",
restartRequired: true,
type: "string",
optional: true,
},
"controller.auth_secret": {
access: ["controller"],
title: "Controller Authentication Secret",
description:
"Secret used to generate and verify authentication tokens. " +
"Should be a long string of random letters and numbers. " +
"Do not share this.",
restartRequired: true,
type: "string",
optional: true,
},
"controller.heartbeat_interval": {
title: "Heartbeat Interval",
description: "Interval heartbeats are sent out on WebSocket connections.",
type: "number",
initialValue: 15,
},
"controller.session_timeout": {
title: "Session Timeout",
description: "Time in seconds before giving up resuming a dropped WebSocket session.",
type: "number",
initialValue: 60,
},
"controller.metrics_timeout": {
title: "Metrics Timeout",
description: "Timeout in seconds for metrics gathering from hosts.",
type: "number",
initialValue: 8,
},
"controller.system_metrics_interval": {
title: "System Metrics Interval",
description: "Interval in seconds to collect and update system metrics for the Web UI",
type: "number",
initialValue: 10,
},
"controller.proxy_stream_timeout": {
title: "Proxy Stream Timeout",
description: "Timeout in seconds for proxy streams to start flowing.",
type: "number",
initialValue: 15,
},
"controller.factorio_username": {
title: "Factorio Username",
description: "Username to authenticate with Factorio API with.",
autoComplete: "section-factorio username",
type: "string",
optional: true,
},
"controller.factorio_token": {
title: "Factorio Token",
description: "Token to authenticate with Factorio API with.",
autoComplete: "section-factorio new-password",
type: "string",
credential: ["controller"],
optional: true,
},
"controller.share_factorio_credentials_with_hosts": {
title: "Share Factorio credentials with Hosts",
description:
"If enabled, the Factorio Username and Token will be shared with hosts and " +
"used in the server settings of instances in this cluster.",
type: "boolean",
initialValue: true,
},
"controller.default_mod_pack_id": {
title: "Default Mod Pack",
description: "Mod pack used by default for instances.",
inputComponent: "mod_pack",
type: "number",
optional: true,
},
"controller.default_role_id": {
title: "Default role",
description: "ID of role assigned by default to new users.",
inputComponent: "role",
type: "number",
optional: true,
initialValue: 1,
},
"controller.mod_portal_cache_duration_minutes": {
title: "Mod Portal Cache Duration",
description: "Duration in minutes to cache mod portal API responses.",
type: "number",
initialValue: 30,
},
"controller.mod_portal_page_size": {
title: "Mod Portal Page Size",
description: "Maximum number of results per page when querying the Factorio mod portal API.",
type: "number",
initialValue: 1000,
},
"controller.allow_remote_updates": {
description: "When true, allows a remote event to trigger a clusterio update via npm",
type: "boolean",
initialValue: true,
readonly: ["controller"],
hidden: true,
},
"controller.allow_plugin_updates": {
description: "When true, allows a remote event to trigger a plugin update via npm",
type: "boolean",
initialValue: true,
readonly: ["controller"],
hidden: true,
},
"controller.allow_plugin_install": {
description: "When true, allows a remote event to trigger a plugin install via npm",
type: "boolean",
initialValue: false,
readonly: ["controller"],
hidden: true,
},
};
}
export interface HostConfigFields {
"host.version": string;
"host.name": string;
"host.id": number;
"host.factorio_directory": string;
"host.mods_directory": string;
"host.instances_directory": string;
"host.controller_url": string;
"host.controller_token": string;
"host.public_address": string;
"host.factorio_port_range": string;
"host.factorio_username": string | null,
"host.factorio_token": string | null,
"host.max_reconnect_delay": number;
"host.allow_remote_updates": boolean;
"host.allow_plugin_updates": boolean;
"host.allow_plugin_install": boolean;
[key: `${string}.load_plugin`]: boolean;
}
/**
* Host Config class
* @extends classes.Config
*/
export class HostConfig extends classes.Config<HostConfigFields> {
declare static fromJSON: configFromJSON<HostConfig>;
declare static fromFile: configFromFile<HostConfig>;
static fieldDefinitions: classes.ConfigDefs<HostConfigFields> = {
"host.version": {
type: "string",
initialValue: "", // Set on start
readonly: ["host"],
hidden: true,
},
"host.name": {
description: "Name of the host",
type: "string",
initialValue: "New Host",
},
"host.id": {
description: "ID of the host",
type: "number",
initialValue: () => Math.random() * 2**31 | 0,
hidden: true,
},
"host.factorio_directory": {
description: "Path to directory to look for factorio installs",
type: "string",
initialValue: "factorio",
},
"host.mods_directory": {
title: "Mods Directory",
description: "Path to directory where mods for instances are cached.",
restartRequired: true,
type: "string",
initialValue: "mods",
},
"host.instances_directory": {
description: "Path to directory to store instances in.",
restartRequired: true,
type: "string",
initialValue: "instances",
},
"host.controller_url": {
description: "URL to connect to the controller at",
restartRequired: true,
type: "string",
initialValue: "http://localhost:8080/",
},
"host.controller_token": {
access: ["host"],
description: "Token to authenticate to controller with.",
restartRequired: true,
type: "string",
initialValue: "enter token here",
},
"host.public_address": {
description: "Public facing address players should connect to in order to join instances on this host",
type: "string",
initialValue: "localhost",
},
"host.factorio_port_range": {
title: "Factorio port range",
description:
"Range of UDP ports to use for game connections. Supports both comma separated values and " +
"ranges separated with a dash.",
type: "string",
initialValue: "34100-34199",
},
"host.factorio_username": {
title: "Factorio Username",
description:
"Username to authenticate with Factorio API with. If set this will be used in the server settings " +
"of all instances on this host.",
autoComplete: "section-factorio username",
type: "string",
optional: true,
},
"host.factorio_token": {
title: "Factorio Token",
description:
"Token to authenticate with Factorio API with. If set this will be used in the server settings " +
"of all instances on this host.",
autoComplete: "section-factorio new-password",
type: "string",
credential: ["host"],
optional: true,
},
"host.max_reconnect_delay": {
title: "Max Reconnect Delay",
description: "Maximum delay to wait before attempting to reconnect WebSocket",
type: "number",
initialValue: 60,
},
"host.allow_remote_updates": {
description: "When true, allows a remote event to trigger a clusterio update via npm",
type: "boolean",
initialValue: true,
readonly: ["host"],
hidden: true,
},
"host.allow_plugin_updates": {
description: "When true, allows a remote event to trigger a plugin update via npm",
type: "boolean",
initialValue: true,
readonly: ["host"],
hidden: true,
},
"host.allow_plugin_install": {
description: "When true, allows a remote event to trigger a plugin install via npm",
type: "boolean",
initialValue: false,
readonly: ["host"],
hidden: true,
},
};
}
export interface InstanceConfigFields {
"instance.name": string;
"instance.id": number;
"instance.assigned_host": number | null;
"instance.auto_start": boolean;
"instance.exclude_from_start_all": boolean;
[key: `${string}.load_plugin`]: boolean;
"factorio.version": string;
"factorio.executable_path": string | null;
"factorio.shutdown_timeout": number;
"factorio.game_port": number | null;
"factorio.host_assigned_game_port": number | null;
"factorio.rcon_port": number | null;
"factorio.rcon_password": string | null;
"factorio.player_online_autosave_slots": number;
"factorio.mod_pack_id": number | null;
"factorio.enable_save_patching": boolean;
"factorio.enable_script_commands": boolean;
"factorio.enable_whitelist": boolean;
"factorio.enable_authserver_bans": boolean;
"factorio.settings": Record<string, unknown>;
"factorio.verbose_logging": boolean;
"factorio.console_logging": boolean;
"factorio.strip_paths": boolean;
"factorio.sync_adminlist": "enabled" | "disabled" | "bidirectional";
"factorio.sync_whitelist": "enabled" | "disabled" | "bidirectional";
"factorio.sync_banlist": "enabled" | "disabled" | "bidirectional";
"factorio.max_concurrent_commands": number;
}
/**
* Instance config class
* @extends classes.Config
*/
export class InstanceConfig extends classes.Config<InstanceConfigFields> {
declare static fromJSON: configFromJSON<InstanceConfig>;
declare static fromFile: configFromFile<InstanceConfig>;
static migrations(config: Static<typeof this.jsonSchema>) {
function boolToEnableDisable(name: string) {
if (config.hasOwnProperty(name) && typeof config[name] === "boolean") {
config[name] = config[name] ? "enabled" : "disabled";
}
}
boolToEnableDisable("factorio.sync_adminlist");
boolToEnableDisable("factorio.sync_whitelist");
boolToEnableDisable("factorio.sync_banlist");
return config;
}
static fieldDefinitions: classes.ConfigDefs<InstanceConfigFields> = {
"instance.name": {
type: "string",
initialValue: "New Instance",
},
"instance.id": {
description: "ID of the instance",
type: "number",
initialValue: () => Math.random() * 2**31 | 0,
hidden: true,
},
"instance.assigned_host": {
type: "number",
optional: true,
hidden: true,
},
"instance.auto_start": {
description: "Automatically start this instance when the host hosting it is started up",
type: "boolean",
initialValue: false,
},
"instance.exclude_from_start_all": {
description: "Exclude this instance from the 'Start all' button operation",
type: "boolean",
initialValue: false,
},
"factorio.version": {
description: "Version of the game to run, use latest to run the latest installed version",
restartRequired: true,
type: "string",
initialValue: "latest",
inputComponent: "target_version",
},
"factorio.executable_path": {
description:
"Relative path from the Factorio installation directory to the executable to run. " +
"Defaults to auto detect the path, only needed in special setups.",
restartRequired: true,
type: "string",
optional: true,
},
"factorio.shutdown_timeout": {
description:
"Timeout in seconds to wait after requesting the server to stop before killing " +
"the process. Set to 0 to disable.",
type: "number",
initialValue: 300,
optional: true,
},
"factorio.game_port": {
description: "UDP port to run game on, uses a port in host.factorio_port_range if null",
restartRequired: true,
type: "number",
optional: true,
},
"factorio.host_assigned_game_port": {
access: ["host"],
type: "number",
optional: true,
},
"factorio.rcon_port": {
description: "TCP port to run RCON on, uses a random port if null",
restartRequired: true,
type: "number",
optional: true,
},
"factorio.rcon_password": {
credential: ["host", "controller"],
description: "Password for RCON, randomly generated if null.",
restartRequired: true,
type: "string",
optional: true,
},
"factorio.player_online_autosave_slots": {
description:
"Rename autosaves where players have been online since the previous autosave into a separate " +
"autosave pool with this many slots. Requires autosaves to be enabled to work. Set to 0 to disable.",
type: "number",
initialValue: 5,
},
"factorio.mod_pack_id": {
title: "Mod Pack",
description:
"Mod pack to use on this server, if not set the default configured on the controller will be used",
inputComponent: "mod_pack",
restartRequired: true,
type: "number",
optional: true,
},
"factorio.enable_save_patching": {
description:
"Patch saves with Lua code. Required for Clusterio integrations, lua modules, and most plugins.",
restartRequired: true,
type: "boolean",
initialValue: true,
},
"factorio.enable_script_commands": {
description:
"Allows achievement breaking commands to be executed over rcon. " +
"Required for Clusterio integrations and most plugins. " +
"This does not prevent players using script commands.",
type: "boolean",
initialValue: true,
},
"factorio.enable_whitelist": {
description: "Turn on whitelist for joining the server.",
restartRequired: true, // Because of the cli option "--use-server-whitelist"
type: "boolean",
initialValue: false,
},
"factorio.enable_authserver_bans": {
description: "Turn on Factorio.com based multiplayer bans.",
restartRequired: true, // Because of the cli option "--use-authserver-bans"
type: "boolean",
initialValue: false,
},
"factorio.settings": {
description: "Settings overridden in server-settings.json",
restartRequired: true,
restartRequiredProps: [
"afk_autokick_interval", "allow_commands", "autosave_interval", "autosave_only_on_server",
"description", "ignore_player_limit_for_returning_players", "max_players", "max_upload_slots",
"max_upload_in_kilobytes_per_second", "name", "only_admins_can_pause_the_game", "game_password",
"require_user_verification", "tags", "visibility",
],
type: "object",
initialValue: {}, // See create instance handler in controller.
},
"factorio.verbose_logging": {
description: "Enable verbose logging on the Factorio server",
restartRequired: true, // Because of the cli option "--verbose"
type: "boolean",
initialValue: false,
},
"factorio.console_logging": {
description: "Enable console logging to a separate file, useful for 3rd party integrations",
restartRequired: true, // Because of the cli option "--console-log"
type: "boolean",
initialValue: false,
},
"factorio.strip_paths": {
description: "Strip down instance paths in the log",
restartRequired: true,
type: "boolean",
initialValue: true,
},
"factorio.sync_adminlist": {
description: "Synchronize adminlist with the controller",
type: "string",
enum: ["disabled", "enabled", "bidirectional"],
initialValue: "bidirectional",
},
"factorio.sync_whitelist": {
description: "Synchronize whitelist with the controller (whitelist must be enabled)",
type: "string",
enum: ["disabled", "enabled", "bidirectional"],
initialValue: "bidirectional",
},
"factorio.sync_banlist": {
description: "Synchronize banlist with the controller",
type: "string",
enum: ["disabled", "enabled", "bidirectional"],
initialValue: "bidirectional",
},
"factorio.max_concurrent_commands": {
description: "Maximum number of RCON commands trasmitted in parallel",
restartRequired: true,
type: "number",
initialValue: 5,
},
};
}
export interface ControlConfigFields {
"control.controller_url": string | null;
"control.controller_token": string | null;
"control.max_reconnect_delay": number;
}
/**
* Control config class
* @extends classes.Config
*/
export class ControlConfig extends classes.Config<ControlConfigFields> {
declare static fromJSON: configFromJSON<ControlConfig>;
declare static fromFile: configFromFile<ControlConfig>;
static fieldDefinitions: classes.ConfigDefs<ControlConfigFields> = {
"control.controller_url": {
description: "URL to connect to the controller at",
type: "string",
optional: true,
},
"control.controller_token": {
access: ["control"],
description: "Token to authenticate to controller with.",
type: "string",
optional: true,
},
"control.max_reconnect_delay": {
title: "Max Reconnect Delay",
description: "Maximum delay to wait before attempting to reconnect WebSocket",
type: "number",
initialValue: 60,
},
};
}
function validateFields(
pluginName: string,
fields: Record<string, classes.FieldDefinition>,
) {
for (const [name, field] of Object.entries(fields)) {
if (!name.startsWith(`${pluginName}.`)) {
throw new Error(
`Expected name of config field '${name}' for ${pluginName} to start with '${pluginName}.'`
);
}
}
}
/**
* Add config fields defined by the provided plugin infos
*
* @param {Array<Object>} pluginInfos - Array of plugin info objects.
*/
export function addPluginConfigFields(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) {
function pluginConfig(
pluginInfo: PluginNodeEnvInfo | PluginWebpackEnvInfo,
kind: "controllerConfigFields" | "hostConfigFields" | "instanceConfigFields",
Config: typeof ControllerConfig | typeof HostConfig | typeof InstanceConfig,
) {
(Config.fieldDefinitions as any)[`${pluginInfo.name}.load_plugin`] = {
title: "Load Plugin",
restartRequired: true,
type: "boolean",
initialValue: true,
};
const fields = pluginInfo[kind];
if (fields) {
validateFields(pluginInfo.name, fields);
Object.assign(Config.fieldDefinitions, fields);
}
}
for (let pluginInfo of pluginInfos) {
pluginConfig(pluginInfo, "controllerConfigFields", ControllerConfig);
if (pluginInfo.hostEntrypoint || pluginInfo.instanceEntrypoint) {
pluginConfig(pluginInfo, "hostConfigFields", HostConfig);
}
if (pluginInfo.instanceEntrypoint) {
pluginConfig(pluginInfo, "instanceConfigFields", InstanceConfig);
}
}
}