Plugins installed from npm are listed by package name, but the plugin
loader joined that name onto the main and entrypoint paths and imported
the result as a file, which resolves relative to the working directory.
Every npm installed plugin then failed to load. Resolve these paths from
the directory of the plugin's package.json instead.
EventSubscriber registers with the control's onAccountUpdate when it
has one and resends its subscription through handleAccountEvent. A lost
permission makes the request fail, a granted one syncs it again.
The web Control also reports the account on connect, so resends are
queued and sent once per tick. A failed subscription request now sets
synced to false and notifies callbacks, before the error never reached
the UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The old name read as an unconditional unsubscribe. Log following has
its own subscription outside SubscriptionController, so check
core.log.follow on permission changes too. Move the per-connection
work into one helper and split the tests into userPermissionsUpdated
and rolesUpdated, checking the account update event is sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add controller.max_send_buffer_size in MiB, default 64 and 0 for no
limit. Close with 1013 instead of 1008 on overflow: clients treat 1008
as fatal, so a host would shut down and the web UI would log out. With
1013 the client resumes, the controller answers with invalidate since
the session has ended, and the client registers a new session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lowering the limit at runtime would leave connections holding more
filters than allowed, so read it once when the controller starts.
Move the limit tests into the subscribe and replace groups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every message sent over a link is kept in the send buffer until the
other side acknowledges it with a heartbeat, so it can be resent if
the session resumes. Nothing forced a peer to acknowledge, so a client
that kept heartbeating without a sequence number while subscribed to a
busy event made the controller buffer messages until it ran out of
memory.
Store buffered messages serialised, track their total size, and close
the connection once it goes over maxSendBufferSize. The controller
sets this to 64 MiB for the connections it accepts. A client that far
behind could not resume in a reasonable time anyway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The permission for a subscription was only checked when subscribing,
so a control connection kept receiving events after the user's roles
or role permissions changed to no longer allow it.
Add SubscriptionController.unsubscribeUnpermitted which rechecks every
subscription of a link and removes the ones the user is no longer
permitted, and call it for the affected control connections from
userPermissionsUpdated and rolesUpdated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A subscriber could keep sending subscribe requests with new filters and
the controller would add every one of them to the subscriber's filter
set without bound, growing memory until it ran out.
Add controller.max_subscription_filters (default 10000) and reject
subscribe and replace requests that would leave a subscriber holding
more filters than that for one event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows this test throws "EPERM: operation not permitted, rename
<path>" presumably because Windows prevents concurrent renames. This
does not affect the test outcome, so ignore the error(s).
These are used outside of the integration tests, but including the
integration index file causes the very slow setup of the contorller
and host. Avoid the integration setup costs when running commands
such as `pnpm mocha test/lib` by moving these utility functions to
the common test functions.
Without a transport Winston will print annoying "Attempt to write logs
with no transports" warnings all over the place. Setup a transport in
tests that cause logging to suppress this.
The behaviour of creating the config file if it's missing while
convenient causes confusion and problems when the tools are run
in the wrong folder. They end up creating a shadow installation
and misdirect the user with nonsensical error messages, instead
pointing to the real problem which is that the tool can't find
the configuration needed to actually run.
Require a config file to be present at startup and refuse to run if this
file is missing. To support the odd use-case of creating the config
when missing a --create-config flag is added. To conveniently generate
a config file to edit out from the "config create" command is added.
Ctl ended up calling its main class for "Control" for historical
reasons, along with its config file and plugin entrypoint.
The plugin entrypoint was renamed to ctl in 808eda0513. This
finishes that renaming by also renaming the main class, default config
file, config-control command, and main entrypoint.
The config-control command is renamed to config to be consistent with
the host and controller's config commands.
The term "control" is still kept in the config and message locations as
a common specifier for both ctl and web_ui.
A permission function runs during ingress permission checking, before
the request body is validated. UserBulkImportRequest.permission read the
raw, unvalidated message.data and threw a bare Error for an unknown
importType. That error is not a PermissionError, so _processMessage
rethrew it to the connector "message" listener, which emitted "error" on
the connector. Controller-side connectors have no "error" listener, so
Node rethrew and handleUnhandledErrors exited the process.
Repro: a control client with client-side validateSent disabled sends
UserBulkImportRequest with importType "foo"; the client gets a normal
RequestError response and the controller then logs a fatal uncaught
exception and exits.
Fix both layers: _processMessage now logs and drops an unexpected error
from the permission check (control connections only, where a response
was already sent) instead of rethrowing, and UserBulkImportRequest
throws a PermissionError for an unknown importType so it is denied
cleanly. A genuine PermissionError still denies silently as before.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Two writes to the same file at the same time shared one temporary name, so the second write clobbered the first's temporary and the first rename left the second call with nothing to rename, failing with ENOENT. The host does exactly that when it saves an instance config while the instance saves it too.
The temporary now carries a random tag and is created exclusively, so concurrent writers never touch each other's file. Since the temporary is always new, the mode is applied on creation and the chmod for reused leftovers goes away. A failed rename removes its temporary instead of leaving it for the next write to overwrite.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Rethrow errors other than ENOENT and ENOTDIR when reading the package
spec as a directory, mark the tests that run npm as slow so FAST_TEST
skips them, and use ./external_plugins/my_plugin in the README examples.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plugins with only host, instance or ctl code have no reason to ship a
webpack build, yet the plugin list showed "Missing dist/web/manifest.json"
as an error for them and the controller logged a warning on every start.
Decide whether a build is expected from the plugin declaration, using the
same rule the create tool uses to decide when to generate one: a web or
controller entrypoint, or any config fields. Plugins outside that rule get
no bundle path and no error, the web UI skips loading them, and the plugin
pages show them as having no web module instead of an error or a prompt to
reload.
Fixes#1004
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Config files hold the auth secret and tokens, so the default 0o644 lets
every user on the system read them. Config.save, the ctl config written
by the controller bootstrap, ctl host create-config and the config
migration in create now pass mode 0o600.
safeOutputFile also applies the mode to the temporary file it writes,
since writeFile only honours mode on creation and a leftover temporary
from a failed write would otherwise keep its old mode.
Closes#1005
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Replace invokeHook and the per plugin on* methods with AsyncHook
collections on the controller, host, instance, clusterioctl and web
control objects. Plugin entrypoints export a default function that
receives a load context and attaches handlers to the hooks it needs.
Class exports still load through fromContext with a deprecation
warning. The create templates and docs use the entrypoint function.
This is clusterio/clusterio#921 recreated on top of master after the
ESM migration and the PluginDeclaration permissions change.
Closes#730
Co-Authored-By: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds a permissions array to PluginDeclaration, a type-level Permissions
registry that plugins extend with module augmentation, and
registerPluginPermissions() called from all entrypoints. checkPermission
and the web UI hasPermission helpers now take PermissionName so typos are
compile errors. Core definitions move into a corePermissions array that
the interface derives its keys from.
Closes#987
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
Migrate the usage of CommonJS specific code and imports to ESM. Roughly
summarised this:
- Changes all require() calls to import statement or expressions.
- Changes usage of module.exports to export statements.
- Replaces require.resolve with import.meta.resolve.
- Replaces conditional imports with conditional subpath imports.
- Replaces module === require.main with import.meta.main.
- Adds the full filename with extention to all relative imports.
- Changes all usage of __dirname and __filename to their import.meta
equivalents.
- Sets "type": "module" in package.json files.
- Removes usage of "use strict".
- Adds .ts an an alias of .js for the webpack build.
Due to differences in how require and import resolves files the plugin
entrypoints will have to include the filename and extension after this
migration in order to work.
resolveHost, resolveInstance, resolveModPack and retrieveRole treated any
integer like argument as an id and never looked it up as a name. An
instance created as "10" could not be referenced by name afterwards. The
lookup now treats the argument as an id only if an entry with that id
exists, and otherwise falls back to a name match.
Closes#886
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Installing a plugin used to take two steps, npm install followed by plugin
add, and both silently did their work in whatever directory they were run
from. The new plugin install <package> subcommand runs npm install --save
and then discovers the plugin through the existing loadPluginList logic,
so the plugin list is updated in the same step.
The command refuses to run outside a Clusterio installation, detected by
the package.json the installer writes or one that depends on a Clusterio
package. plugin add with a bare package name warns in the same situation.
Reinstalling a plugin that is already listed reports it as such instead of
failing. Installing a package without the clusterio-plugin keyword fails.
Fixes#1008
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Updating a single plugin can leave the installation with multiple
versions of @clusterio/lib. Passing the core package and every npm
installed plugin to a single npm update call lets npm resolve them
together.
UpdateAllRequest targets the controller or a host, requires both the
core update permission and core.plugin.update, and is gated by both
allow_remote_updates and allow_plugin_updates. The web UI Updates modal
gets an Update All action and ctl gets --all on the update commands.
Closes#910
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Generic error handlers such as FactorioServer.handle() and the event
handler catch in Link only log err.stack, so the ajv errors attached to
an InvalidMessage were lost unless the catch site checked for the class
explicitly. Appending the errors to the stack in the constructor makes
every stack-logging handler show which field failed validation without
special casing each one.
Fixes#986
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add option in the controller config to set where static assets are
loaded from in order to support deployments where the static assets are
server via a CDN while the API is directly accessed.
This is useful because some CDNs put low limits on how large requests
bodies can be, which causes issues with uploading large mods and saves.
* Support enemy_expansion.build_base_unit_dispatch_cooldown in exchange strings
Factorio 2.1.13 added build_base_unit_dispatch_cooldown as the last
field of enemy_expansion in the map exchange string, and rejects map
settings without it. Track the full version in the reader state and
read the field for 2.1.13 and later. Adds a fixture string generated
with 2.1.14 and uses it in the load-scenario integration test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Replace version flags with versionAtLeast in exchange string reader
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Factorio 2.1.13 added build_base_unit_dispatch_cooldown as the last
field of enemy_expansion in the map exchange string, and rejects map
settings without it. Track the full version in the reader state and
read the field for 2.1.13 and later. Adds a fixture string generated
with 2.1.14 and uses it in the load-scenario integration test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an externalTest(this) helper alongside slowTest that skips the test
when NO_EXTERNAL_TEST is set and raises the timeout to 60s, plus a
no-external-test script. Marks the tests hitting factorio.com, the mod
portal and GitHub with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>