The entry point becomes real: five mount prefixes, the admin.users.detail extension slot, the shard push catalog, the town-crier announce leg and both lifecycle hooks. module.json declares all of it and the loader checks the declaration against what register() actually registers, in both directions. The URLs are byte-identical to the ones core served before the extraction. 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 a module answers now. Require order is load-bearing and the requires are inside register() because of it. Every ported file reaches core through ./core, whose members resolve ctx when called -- but a router does `const express = core.express` at ITS file scope, which runs the moment it is required. Hoisting these to the top of the file breaks the module with an error about ctx being missing, from a file that never mentions it. boot.js takes the eight UO call sites out of core's server.js. One behavioural change, deliberate: uoLinkSocket.start() and the sidecar health probe used to run AFTER the listener bound and now run before it, because onBoot does. start() returns as soon as the reconnecting client is armed, but the probe is a real HTTP call, so it is fired and NOT awaited -- an unreachable sidecar must not hold the site closed. Reporting that the bridge is down is diagnostics; being up is not a precondition for serving a page. router/rateLimits.js builds the market limiter through ctx.middleware.rateLimit, core's factory. The policy is the module's -- only the module knows what its endpoints cost -- and the plumbing is core's, so there is one express-rate-limit in the process and one place a breach is logged. Co-Authored-By: Claude <noreply@anthropic.com>
98 lines
5.0 KiB
JavaScript
98 lines
5.0 KiB
JavaScript
// ── 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
|
|
// `<website>/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,
|
|
})
|
|
}
|