# Module-Rust The **[Rust](https://rust.facepunch.com/) module** for the Runic Gateway platform: everything that makes a Runic Gateway site a site *for* Rust. It installs into a website core as `modules/rust/` and is the platform's second game module, after [`Module-uo`](https://gitea.whitlocktech.com/RunicGateway/Module-uo). It is also the first module built from the [Integration Kit](https://gitea.whitlocktech.com/RunicGateway/Integration-kit) rather than extracted from the website — which makes it the kit's acceptance test from the inside. **The repository name is not the module id.** This ships a module whose `id` is `rust`, because the contract requires `id` to equal the directory core loads it from (`modules/rust/`), and that id is the prefix of every table and every mount. ## What it is, in one diagram ``` Rust server + Oxide (RunicGateway/Rust-Plugins) │ loopback TCP, the plugin dials out ▼ rust-link sidecar (RunicGateway/Rust-Link) one per game server │ HTTPS + WebSocket, bearer token ▼ this module, inside a website core one client per server │ same-origin JSON ▼ browser · Android app ``` **One server, one sidecar.** A community running six Rust servers runs six pairs and configures six rows here; the website core never learns there is more than one. ## What ships today | Surface | Route | |---|---| | Public | `GET /api/v1/public/rust/servers` — every server and what it last reported | | Player | `GET /api/v1/player/rust/servers` — the same, on the authenticated tier | | Admin | `GET/PUT/DELETE /api/v1/admin/rust/servers` and `POST …/:id/test` | | Page | `/rust/servers` | Two tables, `rust_servers` (configuration) and `rust_server_state` (what each sidecar reported). The rest of the module — identity, site-owned permissions, Teams from Rust's clans, notifications, events, the live map, Discord commands — arrives phase by phase. **Nothing is registered before it has something behind it:** a declared trigger nothing emits and a declared slot nothing fills are both surfaces an operator can configure and then wait on, which is worse than an absent one. ## Build and check ```bash npm ci --prefix server && npm test --prefix server npm run check:imports --prefix server npm run check:bundle --prefix server npm run check:swagger --prefix server npm ci --prefix client && npm run build --prefix client npm run check:externals --prefix client && npm test --prefix client ``` **Build the client BEFORE running its tests** — two of them read the built chunk and skip when there is none, so a run in the other order passes while asking nothing about the artifact that ships. Regenerate the OpenAPI fragment whenever a route or an annotation changes: ```bash npm run swagger --prefix server # writes swagger-fragment.json; commit it ``` `.gitea/workflows/pr-checks.yml` runs all of the above on every pull request, plus one job this machine cannot run on its own: **frozen-manifest** clones core at the sha pinned in [`ci/core-ref.json`](ci/core-ref.json), generates its route table without this module and then with it, and takes the difference. That difference is the URL surface this module serves — checked against the committed [`routes.manifest.json`](routes.manifest.json), against the OpenAPI fragment in both directions, and against the rule that **a module may only add**. It is the only thing that can see whether `/rust` collides with one of the routes core mounts at a tier root (`/status`, `/version`), which the loader's own collision probe cannot find. ## How it reaches an operator **An operator never builds anything.** A release is not source: it is the directory core's loader expects at `modules/rust/`, already assembled — the prebuilt client chunk, the schema fragment and the OpenAPI fragment, packed as they will be unpacked. **Every merge to `main` carrying a releasable commit publishes a bundle.** The next version is computed from conventional-commit subjects since the newest `v*` tag, as in `link`, `installer` and `Module-uo`: `feat!:` or `BREAKING CHANGE` is a major, `feat:` a minor, `fix:` or `perf:` a patch, and a `main` that gained none of those cuts no release. The number that ships is the **tag**, and CI writes it into the `module.json` inside the bundle. `module.json`'s version survives as a **floor**: name a version there above the newest tag and that version releases, which is how you overrule the subjects. For a change with nothing releasable behind it — a widened `coreApi`, a new mount, a capability — run the **Release** workflow by hand (Actions → Release → Run workflow). Each release carries: | Asset | What it is | |---|---| | `module-rust-.tar.gz` | the directory core expects at `modules/rust/`, already assembled | | `module-rust-.json` | the install manifest: id, version, `coreApi`, the artifact's URL, size and **`sha256`** | | `SHA256SUMS` | the same hash, in the shape every other repo here publishes | Releases are **unsigned**; the `sha256` is the trust anchor, and the website verifies it before unpacking. That is the model `installer`'s bundles already use, and a second trust model would be a second thing to get right. The tarball is assembled from an **include** list ([`ci/bundle.json`](ci/bundle.json)), never an exclude list — an exclude list ships whatever it forgot. Tests, scripts, `client/src`, `ci/` and the dev dependencies are not in it. It carries **no `node_modules`**, because the shipped half declares no runtime dependencies: everything it needs arrives on `ctx`. `npm run check:bundle` holds both halves of that — that the list still covers every file `server/index.js` can reach, and that no runtime dependency has appeared without the release learning to pack it. ## Install it into a core **From a release**, which is the supported path: in Admin → Modules, paste the URL of that release's `module-rust-.json`, and restart when the panel offers. Core fetches the manifest, checks every URL and redirect hop against its own host allowlist, streams the artifact under a byte cap while hashing it, verifies the `sha256`, inspects the archive in full before unpacking it to a temporary directory, and only then moves it into `modules/rust/`. Nothing is written into the modules directory until every check has passed. The allowlist must contain `gitea.whitlocktech.com` — it is seeded from `MODULE_SOURCE_HOSTS` on a fresh install and is DB-owned from then on, edited on that same screen. **An empty allowlist forbids every install rather than permitting all of them.** **From a working tree**, for development: copy the whole tree to `/modules/rust/` and restart. **Copy, do not symlink** — the loader lists directory entries and asks each whether it is a directory; a symlink answers no and the module is skipped in complete silence. Either way, the module appears when the process restarts: the volume is read at require time. Then, in Admin → Rust, add a server: its name, the sidecar's base URL, and the token the sidecar printed on first start (`rust-link-sidecar --print-config`). **The token is write-only** — it is stored encrypted through core's own secret box and never returned to any client; the panel reports only whether one is set. `POST /api/v1/admin/rust/servers/:id/test` probes a sidecar and reports what came back in one word. That is the route that tells a wrong URL from a wrong token from a mismatched protocol version — all three present as "the site says my server is offline" and each has a different fix. ## The protocol is a contract `PROTOCOL_VERSION` in `server/sidecarClient.js` is sent on every request as `X-RustLink-Version`, and a sidecar speaking a different one answers `409` rather than serving something this module will mis-parse. It must agree with the sidecar's own constant and with `overlay.toml` in the plugin repo. Canonical spec: [`docs/rust-link/PROTOCOL.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/rust-link/PROTOCOL.md). The module's own design of record is [`docs/modules/rust/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust/PLAN.md). ## Licence GPL-3.0-or-later. See [LICENSE.md](LICENSE.md).