Files
Module-uo/server/router/rateLimits.js
wtclaude 740a677f92 feat(server): register the routes, the slot, the leg and the boot hooks
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>
2026-08-11 12:06:46 -05:00

47 lines
2.0 KiB
JavaScript

// This module's own rate-limit policy, built on core's plumbing.
//
// `ctx.middleware.rateLimit` is core's `makeLimiter` (MODULE_API.md §2.3, API
// 1.1.0): the module states the window, the cap and the message, and core
// supplies the `express-rate-limit` instance, its store and the logging that
// records a breach. That division is the point. The policy is the module's —
// only the module knows what its endpoints cost — but there is one limiter
// library in the process and one place a breach is written down. A module that
// resolved `express-rate-limit` for itself would get a second store, and a limit
// enforced by two independent counters is not the limit either of them states.
//
// Built lazily, for the reason `core.js` explains: `core.middleware` resolves
// `ctx`, so touching it at require time would run before `register()`. The
// routers ask for these while they are being built, which is inside
// `register()`, and the result is memoised so a limiter is created once and the
// counter is not reset by a second call.
const core = require('../core')
let limiters = null
function build() {
if (limiters) return limiters
limiters = {
// The player-vendor market search. The first genuinely expensive PUBLIC
// endpoint on the site: every call is a LIKE scan plus a COUNT over the
// listings table, which on a large shard is the biggest table there is, and
// it is anonymous by default. Generous for a human browsing shops (a typed
// search is debounced to one request, and paging is a click), tight enough
// that it cannot be used as a cheap way to load the database.
//
// This lived in core's `middleware/rateLimit.js` and is UO policy, so it
// came here with the route it guards.
marketLimiter: core.middleware.rateLimit({
windowMs: 60 * 1000,
max: 60,
label: 'market',
message: 'Too many searches. Please slow down.',
}),
}
return limiters
}
module.exports = {
get marketLimiter() { return build().marketLimiter },
}