Drop the special case that deferred terminating the connection issuing
the delete, so deleting yourself behaves the same as revoke-token and
every session of the deleted user is closed before the handler returns.
Set tokenValidAfter to the current time in createUser instead of 0.
Deleting a user and recreating it under the same name left tokens issued
before the deletion valid, as the check is iat < tokenValidAfter. Doing
it in createUser covers every path that makes a user. A token signed in
the same second as creation is still accepted, so create-admin and the
other production paths sign after the user exists and are unaffected.
The integration harness signed the control token at import time, before
bootstrap create-admin created the user. Sign it in the root before hook
instead and reuse it in link.js.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ControlConnection keeps the User object it authenticated with and checks
permissions against it. Deleting the user removed the record but left
its sessions open, so a deleted user kept all its permissions until the
WebSocket closed.
Repro: create "victim" with role 0 and connect as it. Another admin
sends UserDeleteRequest("victim"). Victim can still send
UserCreateRequest("mallory") and UserUpdateRolesRequest("mallory", [0]).
Terminate the deleted user's connections like revoke-token does. When
a user deletes itself, defer terminating the requesting connection so
the response is sent first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The close handler removed the log subscription transport from the global
lib logger, but updateLogSubscriptions adds it to the controller's
clusterLogger. Every control connection that subscribed to logs stayed
attached to clusterLogger after closing, keeping the connection alive and
running every later cluster log line through its filter.
handleDebugDumpWsRequest added a new debugEvents listener on each call
and overwrote ws_dumper, so the close handler only removed the last one.
Repeated requests stacked listeners, triggered MaxListenersExceededWarning
after 11 calls, and left the earlier listeners behind. Ignore the request
if the connection is already dumping.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
yargs, @types/yargs, semver and @types/semver are used by several packages at the same version. Also point the remaining direct references to @swc/core and react-router-dom at their existing catalog entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Raise every caret range to the current latest within its major, in the
root package.json, the pnpm-workspace.yaml catalog, the per-package
package.json files and the create plugin template.
Majors taken: eslint 10, @eslint/js 10, globals 17, nyc 18,
@types/express 5, @types/node 24, finalhandler 2, pidusage 4,
style-loader 4, css-loader 7, yargs 18 (all packages), chalk 6.
Majors skipped: mocha 12 (integration suite fails under it, 59 tests,
starting with a 30 s timeout in clusteriocontroller run; kept at
^11.8.0), inquirer (10+ is a rewrite on @inquirer/prompts where
ui.Prompt.prototype.close no longer exists, so the win32 readline
workaround in create.js would have to go and cannot be verified here),
react/antd/react-router-dom/@types/react, typescript 7,
@sinclair/typebox, webpack-merge (exact pin), webpack-dev-middleware,
webpack-manifest-plugin, winston-daily-rotate-file.
Code adaptations: the two /api/stream/:id handlers in routes.ts are
typed Request<{ id: string }> because @types/express 5 types
req.params values as string | string[]; eslint.config.mjs adds
newIsCapExceptions: ["constructor"] because eslint 10.10 new-cap now
reports `new this.constructor()`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The metrics endpoint fans out a gather to every connected host, so
anyone who could reach the controller could read cluster statistics and
put load on all hosts at will. Scraping now needs a user token holding
the new core.controller.metrics permission. The token is accepted via
Authorization: Bearer as well as X-Access-Token because Prometheus can
only send the former. Concurrent scrapes share one gather so hosts see
at most one metrics request at a time.
The three connection gauges in controller.ts now no-op before the
controller exists, like the other gauges there, so the endpoint can be
tested against the mock controller.
Closes#1009
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.
Values in the assetsByChunkName object are arrays not strings. This
caused the dev mode to fail after 2f4e0c4418 added an additional step
filtering out the static/ prefix from what it expected to be a string.
The ESM import of yargs does not support the singleton way of using
yargs, instead we have to call the yargs function to get a parser that
can be augmented with the options.
The .argv property has also been deprecated. Replace it with a call to
.parse() instead.
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>
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.
staticRoot was set to the same value as webRoot, so the static/ prefix
ended up scattered across consumers: index.html relied on the manifest
path containing it, the export stores hardcoded `${staticRoot}static/`,
and bootstrap loaded plugin bundles from webRoot instead. Now staticRoot
is `${webRoot}static/` and the controller strips the static/ prefix from
bundle paths before handing them to the page and to /api/plugins.
Plugins that fetch assets with `${staticRoot}static/...` need to drop the
static/ part.
Fixes#980
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dependency resolver picked the newest release satisfying the version
range regardless of which Factorio version it targeted, so a 2.0 mod pack
could end up with 2.1 dependencies. Only consider portal releases and
local mods whose factorio_version matches the mod pack.
Fixes#965
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cooldude's GameVersion brand was disjoint from the clean version types in
both directions, forcing safeVersion/safeFactorioVersion companions and
breaking widening from FullVersion at file-name call sites.
Make GameVersion a plain string alias: the raw, un-normalised version as the
game reads it. The stricter template-literal types (FullVersion,
MajorMinorVersion, PartialVersion) now widen into it for free, while a
GameVersion still can't be used where a normalised version is required
without going through normaliseGameVersion/integerGameVersion. Runtime
validation stays in GameVersionSchema.
- Drop safeVersion/safeFactorioVersion; ModInfo.version stays raw (verbatim so
it matches the mod's file name), integerVersion normalises via the new
integerGameVersion. factorioVersion reverts to a normalised MajorMinorVersion
since it is never part of a file name.
- ModRecord.version becomes a GameVersion so mod packs round-trip the exact
file-name version.
- ModVersionEquality.testVersion / ModVersionRange.testVersion accept a raw
GameVersion and normalise it; the equality bound stays strictly validated.
- Migration only pads clean partial versions; raw versions are kept verbatim.
- Cover the leading-zeros case: version kept verbatim in the file name while
comparing equal to its normalised form.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Through the creation of this commit, it was discovered that ModInfo.version must be typed as GameVersion due to file names. This results in wide spread propagation of the unsafe type. The full impact including on ModRecord and ModVersion.testVersion have no been implemented until a path forward is decided.
An instance's factorio.version can now target a release channel such as
"stable" or "experimental" (any key from factorio.com/api/latest-releases)
in addition to "latest" and specific X.Y / X.Y.Z versions.
The controller proxies the latest-releases API through a ValueCache and
persists the last successful response to latest-releases.json, serving that
copy when the API is unreachable. On instance start the host resolves a
channel to the concrete version it points to and feeds it into the existing
checkForUpdates/findVersion path, so a channel auto-installs and runs like a
pinned version. The version selector in the web UI lists the available
channels alongside "latest".
Also clarifies the host log/error when a newer version cannot be downloaded
automatically because the host uses a direct (single-version) Factorio
install rather than a versioned directory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These packages are obsoleted by the promises API to the built-in Node fs
module. The fs-extra package is also causing problems with broken APIs
that don't behave the same as Node's fs module, and throwing errors
without code properties on them.
A lot of code is repeated in various different ways in order to handle
streaming content to files without overwriting them, whether it's from
web resources or local streams.
Refactor the handling of these streams to all use the same utility
function for creating a write stream, and for the places downloading
urls use the same downloadFile function.
The handling of exported data such as locales and icons in web_ui was
based on a model where there is only one set of data exported per
cluster. This model changed to have one set of exports per mod pack
when mod packs were introduced, but the existing web_ui code was
bodged to use exports from a "default mod pack" on the controller.
Refactor the code loading exported data to take the mod pack as an
argument for selecting which exported data to load. The different kinds
of prototype metadata that can be loaded is consolidated into one
`useExportPrototypeMetadata` hook. The logic for displaying icons is
put into a `FactorioIcon` component. And the obsolete
`useExportManifest` hook is removed.
These changes allow the `/icon-reference` page to display exports on a
per mod pack basis. Which has now been linked on the bottom of the mod
pack view page.