// ── module-uo's server entry point ───────────────────────────────────────── // // Core requires this file once, synchronously, while `app.js` is still being // required, and calls the exported function with `(ctx, api)`. The normative // contract is docs/website/MODULE_API.md §2.2; the three rules that shape every // line below are worth restating where they will be read: // // 1. **No `await`, and no database.** `scripts/routeManifest.js` and // `swagger/swagger.js` both require core's `app.js` with the pool pointed // at a dead port, so a module that queried at registration time would hang // both. Everything needing a live database is in `onBoot`. // 2. **Never resolve what core owns.** This module lives at // `/modules/uo/`, outside `server/`, so Node's resolver never // reaches core's `node_modules` and `require('express')` fails outright. // express, express-validator, the database, the logger, the middleware and // the rest of §2.3 arrive on `ctx` and are re-exported by `./core`. // 3. **Never reach into core's tree.** No relative path may escape this // module's root; `scripts/checkImports.js` enforces that in CI (§5.1). // // **Require order is load-bearing, and it is why the requires below are inside // the function.** Every ported file reaches core through `./core`, whose members // resolve `ctx` when called — but a router does `const express = core.express` at // its own file scope, which runs the moment it 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 would break the module with an error about `ctx` // being missing, from a file that never mentions it. 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 publicShard = require('./router/public/shard.router') const publicAtlas = require('./router/public/atlas.router') const adminShard = require('./router/admin/shard.router') const adminUoLink = require('./router/admin/uoLink.router') const playerShard = require('./router/player/shard.router') const usersShardExtension = require('./router/admin/usersShard.router') const shardStreams = require('./config/shardStreams') const townCrierLeg = require('./utils/shardAnnounce') const boot = require('./boot') /* eslint-enable global-require */ const log = core.logger() // The five prefixes, exactly the ones `module.json` declares — the loader // compares the two and rejects a mismatch in either direction. Each router // mounts INSIDE its tier, so it structurally cannot reach above its prefix, // and the tier's own gate is already applied: `/admin` sits behind // `noindex, isLoggedIn, requireRole(...)`, `/player` behind // `noindex, requireAuth`, `/public` behind nothing by design. // // The URLs these produce are byte-identical to the ones core served before the // extraction (§1.2). That is the whole point of moving the code and not the // paths: the shipped Android app calls `POST /api/v1/admin/shard/kick`, and the // Discord bot reads `/api/v1/public/shard/*`, and neither knows or needs to // know that a module answers now. api.registerRoutes({ public: { '/shard': publicShard, '/atlas': publicAtlas }, admin: { '/shard': adminShard, '/uo-link': adminUoLink }, player: { '/shard': playerShard }, }) // The six `/admin/users/:id/shard/*` URLs, which hang off a CORE resource and // therefore cannot be a mount of our own (§1.9). Core declares the slot in // `users.router.js` and we fill it; the router gets `req.params.id` from the // parent via `mergeParams`. Core's own routes on the resource win any path // conflict, which is correct — it owns the user. api.registerExtension('admin.users.detail', usersShardExtension) // The push catalog and the news leg. Core kept the push infrastructure and the // announce worker; what it never had was an opinion about *shard* streams or // about talking to a town crier, and those are content (MODULE_SYSTEM.md §1.8). // // Seven of these stream ids and the leg id `towncrier` are grandfathered // (§6.5) — they are stored in `notification_subs` and `announce_job_legs.leg` // and read by the shipped Android app, so a rename here is a data migration // plus a client break rather than a tidy-up. api.registerNotificationStreams(shardStreams.STREAMS) api.registerAnnounceLeg(townCrierLeg.leg) api.onBoot(boot.onBoot) api.onShutdown(boot.onShutdown) log.info('registered', { version: require('../module.json').version, routes: 'public:/shard,/atlas admin:/shard,/uo-link player:/shard', streams: shardStreams.STREAMS.length, }) }