// ── 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 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 }, }) // 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 Team provider, the event // triggers and audiences, the engagement seeds, the four event catalogues, the // notification streams, the slash commands and the two extension slots — 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', }) }