ACCOUNT_DELETION.md §4.4 (AD10). api.onUserDeleting calls links.unlinkForDeletion, which runs the player's own unlinkOwned per link, logs rust.account.unlink with reason 'account deleted', and marks every server's permission sync dirty so the withdrawn grants reach the game on the next tick. coreApi ^1.20.0; core-ref pinned to website#232's merge (7bd93b8); engagement manifest regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
261 lines
15 KiB
JavaScript
261 lines
15 KiB
JavaScript
// ── 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
|
|
// `<website>/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 eventLeases = require('./eventLeases')
|
|
const eventWorld = require('./eventWorld')
|
|
const eventRewards = require('./eventRewards')
|
|
const eventWorldCall = require('./eventWorldCall')
|
|
const eventDangerousTreasures = require('./eventDangerousTreasures')
|
|
const eventGuardedCrate = require('./eventGuardedCrate')
|
|
const eventRaidableBases = require('./eventRaidableBases')
|
|
const eventPermRewards = require('./eventPermRewards')
|
|
const eventCommands = require('./eventCommands')
|
|
const eventTemplates = require('./eventTemplates')
|
|
const boot = require('./boot')
|
|
const commands = require('./commands')
|
|
/* 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.
|
|
//
|
|
// The announce leg arrived with phase 13b (D62, D104), when there was a chat
|
|
// verb to deliver through; it is registered below with the rewards. There is
|
|
// still no post hook: nothing in game mirrors a post as state.
|
|
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)
|
|
|
|
// Sign in with Steam (MODULE_API 1.17.0; PLAN_REDESIGNS §15). A Steam account
|
|
// linked on the site is a linked Rust player (D392): core asks before it writes
|
|
// the sign-in identity, and this module refuses a Steam id that is another
|
|
// account's Rust link (D396); once it is written, the Rust link is made. Both
|
|
// are in the links model beside `/link`'s own write, and neither runs at
|
|
// registration — core calls them from a signed-in user's link flow.
|
|
const links = require('./model/links/links.model') // eslint-disable-line global-require
|
|
api.onIdentityLinking(links.checkSteamSignin)
|
|
api.onIdentityLinked(links.linkFromSteamSignin)
|
|
|
|
// Account deletion (MODULE_API 1.20.0; ACCOUNT_DELETION.md §4.4, AD10). Core
|
|
// asks before the users row goes: every Steam id the account holds is unlinked
|
|
// through the player's own unlink, and the permission sync is marked dirty so
|
|
// the withdrawn grants reach the game on the next tick. It cannot stop the
|
|
// deletion, and never waits on a game.
|
|
api.onUserDeleting(links.unlinkForDeletion)
|
|
|
|
// The public demo's seed (MODULE_API 1.18.0, DM15): the rig's server row and
|
|
// demo-player's Steam link, written by this module from its part of the
|
|
// fixture. Called only by `npm run seed:demo` on a DEMO_MODE deployment. The
|
|
// demo's other locks are `demoLocks` in module.json and `demo.js`.
|
|
const demoSeed = require('./demoSeed') // eslint-disable-line global-require
|
|
api.onDemoSeed(demoSeed.onDemoSeed)
|
|
|
|
// The leases (PLAN.md §27, protocol 8): what an event may BORROW on a server
|
|
// and must give back. Core's `core.lease` is the verb; these are the values it
|
|
// may name and the four callables each ships. Every lease is targeted and the
|
|
// target names the server (D73), which is how one value on one server gets
|
|
// exactly one holder without core learning what a server is.
|
|
//
|
|
// The option sources are the three targets' own (D78).
|
|
api.registerEventLeases(eventLeases.LEASES)
|
|
|
|
// The world verbs (PLAN.md §28, protocol 9): what an event MAKES and gives
|
|
// back — a zone, crates, NPCs — and the budgets that price them, each declared
|
|
// beside the verb that spends it (D79, D89). A lease spends none of them.
|
|
//
|
|
// The rewards (PLAN.md §29, protocol 10) join them: the tally, the kit reward
|
|
// and the chat line, with the two budgets they spend. Registered in the same
|
|
// calls' neighbours, not merged into eventWorld's arrays, so each file keeps
|
|
// its own statement of what it declares.
|
|
//
|
|
// Permissions as rewards (PLAN_REDESIGNS.md §12) beside them: any registered
|
|
// permission, lent until the run ends or given for good, each on its own budget.
|
|
//
|
|
// Calling one of Rust's own world events (PLAN_REDESIGNS.md §14.3.1, protocol
|
|
// 14) beside them: kept when the run ends, on a budget of its own. Starting a
|
|
// Dangerous Treasures event (§14.3.4) and placing a guarded crate (§14.3.5)
|
|
// likewise, each on its own budget; and console commands an author typed
|
|
// (§16, D382-D385), priced in commands; and spawning a RaidableBases base
|
|
// (§11.4), ledgered like a zone when the raids helper makes it the run's.
|
|
api.registerEventBudgets([...eventWorld.BUDGETS, ...eventRewards.BUDGETS, ...eventPermRewards.BUDGETS, ...eventWorldCall.BUDGETS, ...eventDangerousTreasures.BUDGETS, ...eventGuardedCrate.BUDGETS, ...eventCommands.BUDGETS, ...eventRaidableBases.BUDGETS])
|
|
api.registerEventActions([...eventWorld.ACTIONS, ...eventRewards.ACTIONS, ...eventPermRewards.ACTIONS, ...eventWorldCall.ACTIONS, ...eventDangerousTreasures.ACTIONS, ...eventGuardedCrate.ACTIONS, ...eventCommands.ACTIONS, ...eventRaidableBases.ACTIONS])
|
|
|
|
// The templates (PLAN_REDESIGNS.md §2.5, MODULE_API 1.13.0): a kit weekend
|
|
// as a starting point on core's New event screen. Core keeps nothing about a
|
|
// template; what it builds is an ordinary draft.
|
|
api.registerEventTemplates(eventTemplates.TEMPLATES)
|
|
|
|
// News in game chat (D104). Core enqueues every registered leg for every
|
|
// published post, so the leg itself sends only to the servers whose switch an
|
|
// operator turned on — off by default, and a server that is down is skipped.
|
|
api.registerAnnounceLeg(eventRewards.LEG)
|
|
|
|
// ONE call for every option source: core takes a batch once, as this module's
|
|
// complete statement, and refuses a second.
|
|
api.registerEventOptionSources([
|
|
...eventLeases.OPTION_SOURCES,
|
|
...eventWorld.OPTION_SOURCES,
|
|
...eventRewards.OPTION_SOURCES,
|
|
...eventPermRewards.OPTION_SOURCES,
|
|
...eventWorldCall.OPTION_SOURCES,
|
|
...eventDangerousTreasures.OPTION_SOURCES,
|
|
...eventGuardedCrate.OPTION_SOURCES,
|
|
...eventRaidableBases.OPTION_SOURCES,
|
|
])
|
|
|
|
// The slash commands (phase 16, R11). The handlers run HERE, in the website
|
|
// process; the bot pulls the definitions and dispatches each call back. Every
|
|
// command is `access: 'everyone'` and resolves its own audience gate inside the
|
|
// handler, and every answer narrower than public is sent to the caller alone
|
|
// (D127) — see `commands/common.js`.
|
|
api.registerSlashCommands(commands)
|
|
|
|
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,
|
|
leases: eventLeases.LEASES.length,
|
|
actions: eventWorld.ACTIONS.length + eventRewards.ACTIONS.length + eventPermRewards.ACTIONS.length + eventWorldCall.ACTIONS.length + eventDangerousTreasures.ACTIONS.length + eventGuardedCrate.ACTIONS.length + eventCommands.ACTIONS.length + eventRaidableBases.ACTIONS.length,
|
|
budgets: eventWorld.BUDGETS.length + eventRewards.BUDGETS.length + eventPermRewards.BUDGETS.length + eventWorldCall.BUDGETS.length + eventDangerousTreasures.BUDGETS.length + eventGuardedCrate.BUDGETS.length + eventCommands.BUDGETS.length + eventRaidableBases.BUDGETS.length,
|
|
announceLeg: eventRewards.LEG.leg,
|
|
commands: commands.map((cmd) => cmd.name).join(' '),
|
|
optionSources: eventLeases.OPTION_SOURCES.length + eventWorld.OPTION_SOURCES.length + eventRewards.OPTION_SOURCES.length + eventPermRewards.OPTION_SOURCES.length + eventWorldCall.OPTION_SOURCES.length + eventDangerousTreasures.OPTION_SOURCES.length + eventGuardedCrate.OPTION_SOURCES.length + eventRaidableBases.OPTION_SOURCES.length,
|
|
})
|
|
}
|