// ── 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 { TRIGGERS } = require('./engagement/triggers') const { STREAMS } = require('./engagement/streams') const { AUDIENCES } = require('./engagement/audiences') const seeds = require('./engagement/seeds') 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) // Notifications and engagement (R7, PLAN.md §25). Four registrations that are // one decision, because they only mean something together: // // triggers what can happen, what a template may say about it, and the // widest audience a rule on it may EVER have — the security // boundary; core refuses a rule that widens a ceiling // streams which of those may reach a phone. Core pushes an engagement // rule only to devices subscribed to a stream of the SAME id, so // a trigger missing here can never buzz anybody (D65) // audiences named sets of people over this module's data, for an operator // to point a rule at; each answers user ids and nothing else // seeds the two bodies worth writing, and one disabled rule group per // family — installing this module mails nobody // // What fires them is `engagement/emit.js`, off the ingest cursor and the // refresh. Registration is a claim, not a call: nothing here touches the // database, and the seeds are written by core after the schema is up. // // **Not registered, and that is D62:** no announce leg and no post hook. Both // need something in game to deliver to, and phase 10 reaches no game. api.registerEventTriggers(TRIGGERS) api.registerNotificationStreams(STREAMS) api.registerAudiences(AUDIENCES) api.registerEngagementSeeds({ templates: seeds.TEMPLATES, ruleGroups: seeds.RULE_GROUPS }) // 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 four event catalogues, the // announce leg 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', triggers: TRIGGERS.length, streams: STREAMS.length, audiences: AUDIENCES.length, }) }