// ── 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 shardTriggers = require('./config/shardTriggers') const shardAudiences = require('./config/shardAudiences') const engagementSeeds = require('./config/engagementSeeds') const townCrierLeg = require('./utils/shardAnnounce') const teamProvider = require('./model/teamProvider/teamProvider.model') const guildCommand = require('./commands/guild.command') 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) // The engagement contract (MODULE_API 1.7.0, ENGAGEMENT.md Phase 11). Triggers // are PAYLOAD contracts: what a rule may fire on, what a template may // interpolate, and — the part that is a security boundary — the widest audience // an operator may ever give each one. `uo.cheat.detected` ceilings at `staff` // and the three operator-facing ones at `admin` (added to the lattice in 1.8.0), // and core refuses a rule that widens either. // // **Triggers and notification streams share ONE id namespace** (§7.2), so this // registration and the one above are two facets of one space and core enforces // that an id has exactly one owner across both. None of the ids below reuses a // stream id: the stream catalog keeps its seven grandfathered names and these // are the `uo.*`-prefixed ones §8.6 specifies. A trigger-only id gets email and // in-app preferences and no push toggle, which is correct — there is nothing to // push it to, and the shipped Android client's catalog is unchanged. api.registerEventTriggers(shardTriggers.TRIGGERS) // Audiences are named sets of PEOPLE an operator composes rules and segments // out of (§5.1a). Their own id space, and their own ceiling arithmetic: a // composition takes the narrowest ceiling it contains, never the widest. // // Registration is a claim; nothing resolves until the engine asks, which is // after `onBoot` — and it must be, because every resolver reads the database // and registration must not (§2.2 rule 1). api.registerAudiences(shardAudiences.AUDIENCES) // What this module SHIPS behind those two (MODULE_API 1.9.0, ENGAGEMENT.md // Phase 11b): sixteen in-universe message bodies on two channels each, and // twenty-five rules — every one of them `enabled = 0`, which the registry // enforces rather than trusts. // // **A catalogue an operator turns on, not a switch that fires on upgrade.** // Nothing here mails anybody: a rule that is off produces nothing, and a rule // that is on still passes the ceiling, the per-user preference, the suppression // list and the verification gate before anything is sent — all of them core's. // // The nine security and operational triggers point at core's generic bodies // (decision 9). A cheat report should read like a cheat report. // // ONE rule group, and the choice is deliberate: a group is seeded once, so a // twenty-sixth rule appended to `triggers-v1` in a later version would reach // fresh installs ONLY. A future trigger wants its own group key. api.registerEngagementSeeds({ templates: engagementSeeds.TEMPLATES, ruleGroups: engagementSeeds.RULE_GROUPS, }) // Teams: a UO guild is a Team, and this module is the authoritative source of // them for this deployment (MODULE_API 1.6.0). Core asks the three questions; // everything about what a guild IS stays here. // // Registration is a claim, not a call — nothing below runs until core // reconciles, which is after `onBoot`. That matters because every method reads // the database, and registration must not. api.registerTeamProvider(teamProvider) // `/guild` — the chat surface for the same guilds (MODULE_API 1.6.0, TEAMS.md // §7.1). The definition travels to the bot; the handler stays here and runs in // the website process, because the bot container has no `modules` volume and // cannot load a line of this module's code. // // Core registers NO commands of its own. "Guild" is this module's word — core // does not own it on a page (phase 3) and does not publish it in a channel // either. api.registerSlashCommands([guildCommand]) 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, triggers: shardTriggers.TRIGGERS.length, audiences: shardAudiences.AUDIENCES.length, }) }