// ── The server entry point ───────────────────────────────────────────────── // // Core requires this file once, synchronously, while its own `app.js` is still // being required, and calls the exported function with `(ctx, api)`. That is the // entire server-side handshake: everything this module can reach arrives on // `ctx`, and everything it can offer is registered through `api`. // // Normative: MODULE_API.md §2.2 (the entry point) and §2.4 (what you register). // // ── Three rules, and each one has a failure behind it ────────────────────── // // 1. **No `await`, and no database.** Core requires `app.js` in two build tools // with the connection pool pointed at a dead port — the route-manifest // generator and the OpenAPI generator both do it — so a module that queried // at registration time would hang both. Anything that needs a live database // goes in `onBoot`, which runs after the schema is up. // // 2. **Never resolve what core owns.** This module lives at // `/modules/rust/`, outside core's `server/`, so Node's resolver // never reaches core's `node_modules` and `require('express')` from here // simply fails. express, express-validator, the database, the logger and the // middleware all arrive on `ctx` (§2.3) and are re-exported by `./core`. A // second express in the process would be a second `Router` prototype, exactly // as a second React would be a second renderer. // // 3. **Never reach into core's tree.** No relative path may escape this module's // root. `scripts/checkImports.js` enforces it (§5.1) and CI runs it. // // ── Why the requires are INSIDE the function ─────────────────────────────── // // Every file below reaches core through `./core`, whose members resolve `ctx` // when they are CALLED. But a router writes `const express = core.express` at its // own file scope, and that runs the moment the file is required. So // `core.init(ctx)` has to happen before the first `require` of anything under // `router/`. Hoisting these to the top of the file breaks the module with an // error about a missing `ctx`, thrown from a file that never mentions one. // // Node caches modules, so requiring here costs nothing after the first call. const core = require('./core') /** * @param {object} ctx what core hands the module (MODULE_API.md §2.3), frozen * @param {object} api what the module registers (§2.4) */ module.exports = function register(ctx, api) { core.init(ctx) /* eslint-disable global-require */ const publicRust = require('./router/public/rust.router') const playerRust = require('./router/player/rust.router') const adminRust = require('./router/admin/rust.router') const usersRust = require('./router/admin/usersRust.router') const teamProvider = require('./model/clans/teamProvider') const boot = require('./boot') /* eslint-enable global-require */ const log = core.logger() // One prefix, on each of the three tiers (R14). The keys here must match // `module.json`'s `mounts` exactly — the loader compares the two and rejects a // mismatch in EITHER direction, so a route never declared and a prefix declared // and never registered both fail loudly at boot rather than quietly at runtime. // // Each router sits INSIDE its tier router, so it structurally cannot reach // above its prefix, and the tier's gate is already applied: `public` is behind // nothing by design, `admin` behind `noindex, isLoggedIn, requireRole(...)` and // `player` behind `noindex, requireAuth`. Per-route gates go on top; the tier // gate is never re-implemented. // // **Prefixes share ONE namespace with core's own, and the collision probe // cannot see all of it.** Core answers several public routes mounted at the // tier root rather than under a prefix — `/status` and `/version` among them — // and the loader's check cannot find those. `/rust` collides with nothing on // any of the three tiers, checked against core's mount tables rather than // assumed. api.registerRoutes({ public: { '/rust': publicRust }, player: { '/rust': playerRust }, admin: { '/rust': adminRust }, }) // R13's first extension slot (§2.4). Core declares `admin.users.detail` on // `/api/v1/admin/users/:id` and we fill it; the router receives the parent's // `req.params.id` through `mergeParams`. Core's own routes on the resource are // declared before the slot is mounted, so core wins any path conflict — it owns // the user, and this module owns what it can say about one. // // **It is declared twice, in two different places, on purpose.** This call is // the SERVER half and `module.json`'s `extensions` array is held against it by // the loader. The CLIENT half is `registry.registerExtension(ID, // 'admin.users.detail', …)` in `entry.jsx` and must NOT appear in that array — // phase 1 found that the hard way with `site.footer.status`, which is a client // slot and fails the load outright when named there. api.registerExtension('admin.users.detail', usersRust) // Teams (R5, PLAN.md §24). A first-party Rust clan is a Team, and this module // becomes the deployment's one authoritative source of them. Core asks; the // provider answers from the clan boards (`model/clans`), and refuses rather // than guessing whenever no board is current. // // **One provider per deployment**, so a site running module-uo as well cannot // have both — the second registration is a collision core reports against the // module that made it. That is core's rule and a real constraint on a mixed // UO + Rust site; it is recorded in §24 rather than worked around here. api.registerTeamProvider(teamProvider) // The lifecycle hooks (§2.5). `onBoot` runs after core's schema, after this // module's schema fragment, and BEFORE the HTTP listener binds — so a module // that must not serve traffic until it has warmed a cache gets that for free. // It has no timeout, deliberately: a slow boot delays the listener, which is the // guarantee rather than a problem to be timed out. // // `onShutdown` runs while core's database pool and push dispatcher are still // open, because flushing through them is the only thing it is for. It gets a // five-second budget and is abandoned past it. api.onBoot(boot.onBoot) api.onShutdown(boot.onShutdown) // Everything else this module will register — the event triggers and // audiences, the engagement seeds, the four event catalogues, the // notification streams and the slash commands — is deliberately absent. Each // arrives with the phase that has something real to put in it. A registration // with nothing behind it is worse than a missing one: a declared trigger // nothing emits and a declared slot nothing fills are both surfaces an operator // can configure and then wait on. log.info('registered', { version: require('../module.json').version, routes: 'public:/rust player:/rust admin:/rust', extensions: 'admin.users.detail', teams: 'first-party clans', }) }