// Public · Shard — token-free, same-origin reads of the live shard. The // status/feed/economy/idoc/champs/guilds/governors/presence/houses endpoints read // the site's own ingested data; nothing here round-trips the sidecar per request. // // Mounted at /api/v1/public/shard by public/index.js. Deliberately NOT site-mode // gated — shard status is useful (and wanted) while the site itself is in // maintenance. // // **GET /shard/stream stays anonymous.** It is consumed by logged-out browser // visitors *and* by the Android ShardStreamClient, neither of which sends an // Authorization header; adding requireAuth here blacks out the public live boards // on web and mobile. The sensitive kinds (staff audit, cheat detection, login // attempts, IPs) are withheld by utils/shardBroadcast.js, not by a route gate — // that per-frame filtering is the security boundary, not this file. /stream is // deliberately NOT wrapped in requireFeature either: it spans every feature, and // each frame is gated individually against the subscriber's rung. // // Every other route carries `requireFeature()` (utils/shardVisibility.js), // which 404s when an admin has disabled the feature and 403s when the caller sits // below its configured audience. Defaults reproduce pre-v3 behavior exactly, so // these gates are inert until an admin changes something. const core = require('../../core') const express = core.express const { param, query } = core.validator const shard = require('./shard.controller') const { validate } = core.middleware const { marketLimiter } = require('../rateLimits') const { requireFeature } = require('../../utils/shardVisibility') const shardRouter = express.Router() shardRouter.get( '/status', requireFeature('status'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Shard connection state, online count and latest economy' /* #swagger.responses[200] = { description: 'Shard status', content: { "application/json": { schema: { $ref: "#/components/schemas/UoShardStatus" } } } } */ shard.getStatus, ) shardRouter.get( '/feed', requireFeature('activity'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Recent notable shard events (from the ingested log)' // #swagger.description = 'The stored-history twin of /shard/stream, and it reaches the same verdict: which kinds are returned is resolved against the caller\'s audience rung under the live visibility config, and each event\'s payload is field-projected against its own kind\'s feature. Kinds the caller may not read are omitted (an explicit ?kind= for one of them returns []), and acct/webId never appear below admin.' // #swagger.parameters['kind'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Filter to a single event kind, e.g. vendor.sale. Returns [] if the caller may not read that kind.' } // #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Max rows (default 100, max 1000).' } /* #swagger.responses[200] = { description: 'Events, newest first', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/UoShardEvent" } } } } } */ query('kind').optional({ values: 'falsy' }).isString().isLength({ max: 48 }), query('limit').optional().isInt({ min: 1, max: 1000 }), validate, shard.getFeed, ) shardRouter.get( '/economy', requireFeature('status'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Gold-supply time series (oldest → newest)' // #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Max samples (default 100, max 1000).' } /* #swagger.responses[200] = { description: 'Economy samples', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/UoShardEconomyPoint" } } } } } */ query('limit').optional().isInt({ min: 1, max: 1000 }), validate, shard.getEconomy, ) shardRouter.get( '/online', requireFeature('presence'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Staff online now (linked staff accounts; location is admin/moderator-only)' /* #swagger.responses[200] = { description: 'Online players', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/UoShardOnlinePlayer" } } } } } */ shard.getOnline, ) shardRouter.get( '/idoc', requireFeature('houses'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Houses currently in danger (IDOC)' // #swagger.description = 'Location-level board of the houses about to collapse. Owner identity and price are gated by the `houses` feature\'s field rules (default `staff`), and the owner\'s game account is admin-only always — so an anonymous caller sees name, region and coordinates only.' /* #swagger.responses[200] = { description: 'IDOC houses', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/UoShardHouse" } } } } } */ shard.getIdoc, ) shardRouter.get( '/champs', requireFeature('champs'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Current champion-spawn board (all categories)' // #swagger.description = 'The live board of every champion / mini-champ / sea-boss spawn. Update in place via the champ.update / champ.remove frames on /shard/stream.' /* #swagger.responses[200] = { description: 'Champion spawns, ordered by name', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ shard.getChamps, ) shardRouter.get( '/guilds', requireFeature('guilds'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Current guild board (rosters, alliances, leaders)' // #swagger.description = 'The live board of every guild. Update in place via the guild.update / guild.remove / guild.join frames on /shard/stream.' /* #swagger.responses[200] = { description: 'Guilds, ordered by name', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ shard.getGuilds, ) shardRouter.get( '/governors', requireFeature('governors'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Current town-governor board (City Loyalty)' // #swagger.description = 'One entry per city with its governor and election phase. Empty if the shard does not run the City Loyalty system. Live via city.update on /shard/stream.' /* #swagger.responses[200] = { description: 'Cities, ordered by name', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ shard.getGovernors, ) shardRouter.get( '/governors/:city/history', requireFeature('governors'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Governor term history for a city' // #swagger.parameters['city'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'City name, e.g. Britain.' } // #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Max terms (default 100, max 500).' } /* #swagger.responses[200] = { description: 'Terms, newest first', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ param('city').isString().isLength({ min: 1, max: 40 }), query('limit').optional().isInt({ min: 1, max: 500 }), validate, shard.getGovernorHistory, ) shardRouter.get( '/presence', requireFeature('presence'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Online population aggregate (count + per-facet + per-region)' // #swagger.description = 'The latest presence.online snapshot powering the "Players Online" widget. Live via presence.online on /shard/stream.' /* #swagger.responses[200] = { description: 'Population snapshot', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ shard.getPresence, ) shardRouter.get( '/houses', requireFeature('houses'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'House registry (owner, co-owners, price, decay)' // #swagger.description = 'Every house seen via the house.update registry feed. `price` is the placement value, not a for-sale flag. Live via house.update / house.remove on /shard/stream.' /* #swagger.responses[200] = { description: 'Houses, ordered by name', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/UoShardHouse" } } } } } */ shard.getHouses, ) shardRouter.get( '/ruleset', requireFeature('ruleset'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'The shard\'s published ruleset (expansion, systems, caps, limits)' // #swagger.description = 'How this shard is actually configured, published by the shard itself as one world.ruleset frame: expansion, which optional systems are on, skill/stat caps, account and house limits, champion scroll rules and the save/restart schedule. Served from our own store, so it renders while the shard is down; live via world.ruleset on /shard/stream. Returns `null` if the shard has never published one (an older plugin, or Bridge.RulesetEnabled=false) — distinct from a published ruleset, and the page renders it differently.' /* #swagger.responses[200] = { description: 'The ruleset, or null if never published', content: { "application/json": { schema: { type: "object", nullable: true, additionalProperties: true } } } } */ shard.getRuleset, ) shardRouter.get( '/points', requireFeature('leaderboards'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Points / loyalty leaderboards, one board per point system' // #swagger.description = 'Every points/loyalty leaderboard the shard publishes (Queen\'s Loyalty, Void Pool, the nine city loyalties, Clean Up Britannia, …), each with its display name, max points, participant count and top N. Served from our own store, so it renders while the shard is down; live via points.board on /shard/stream. A board\'s display name may arrive as a literal (`nameString`) or a cliloc id (`nameNumber`) — resolve clilocs client-side.' /* #swagger.responses[200] = { description: 'Boards, ordered by display name', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/UoShardPointsBoard" } } } } } */ shard.getPointsBoards, ) shardRouter.get( '/points/:system', requireFeature('leaderboards'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'One points system\'s leaderboard' // #swagger.description = 'A single board by the shard\'s own PointsType name (e.g. `QueensLoyalty`, `CleanUpBritannia`). Returns 404 when the shard has never published that system — distinct from a published board that nobody has scored in yet, which returns 200 with an empty `top`.' /* #swagger.parameters['system'] = { in: 'path', required: true, description: 'PointsType name, e.g. QueensLoyalty', schema: { type: 'string' } } */ /* #swagger.responses[200] = { description: 'The board', content: { "application/json": { schema: { $ref: "#/components/schemas/UoShardPointsBoard" } } } } */ /* #swagger.responses[400] = { description: 'Malformed system name' } */ /* #swagger.responses[404] = { description: 'The shard has never published that system' } */ shard.getPointsBoard, ) // ── Marketplace ──────────────────────────────────────────────────────────── // // Rate-limited, unlike every other route in this file. These are the first // genuinely expensive PUBLIC reads on the site — a LIKE scan plus a COUNT over // what is typically the largest shard_* table, reachable with no session. shardRouter.get( '/market', requireFeature('market'), marketLimiter, // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Search the player-vendor marketplace' // #swagger.description = 'Every priced listing on every player vendor the shard publishes — the same index the in-game Vendor Search gump reads, and it honours the same per-vendor opt-out, so a player who hid their shop in game is hidden here too. Results are LISTINGS, each carrying enough of its shop to be actionable. Served from the site\'s own tables (the sidecar is not touched), so it renders while the shard is down; `staleAt` is the oldest vendor row and the page must say how far behind the index can be — the shard sweeps vendors round-robin, so prices are inherently up to one full cycle old. Item names are resolved server-side against the cliloc table (docs/website/CLILOCS.md); on a shard that has not configured one, `displayName` is null and clients render the item id.' // #swagger.parameters['q'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Substring match on the resolved item name or the item\'s own literal name (max 60 chars).' } // #swagger.parameters['minPrice'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Lowest price to include.' } // #swagger.parameters['maxPrice'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Highest price to include.' } // #swagger.parameters['itemId'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Exact ItemID (art id) match — the more-like-this filter.' } // #swagger.parameters['map'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one facet. Facet names come from the shard\'s own data; an unknown one returns an empty page.' } // #swagger.parameters['region'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one named region.' } // #swagger.parameters['sort'] = { in: 'query', required: false, schema: { type: 'string', enum: ['price_asc','price_desc','recent'] }, description: 'Default price_asc. recent orders by when the shop was last seen.' } // #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Page size, 1..100 (default 50).' } // #swagger.parameters['offset'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Rows to skip (default 0).' } /* #swagger.responses[200] = { description: 'A page of listings plus the unpaginated total and the staleness stamp', content: { "application/json": { schema: { $ref: "#/components/schemas/UoShardMarketPage" } } } } */ /* #swagger.responses[403] = { description: 'The market feature is gated above this caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[404] = { description: 'The market feature is disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[429] = { description: 'Rate limited', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ query('q').optional({ values: 'falsy' }).isString().isLength({ max: 60 }), query('minPrice').optional({ values: 'falsy' }).isInt({ min: 0, max: 999999999 }), query('maxPrice').optional({ values: 'falsy' }).isInt({ min: 0, max: 999999999 }), query('itemId').optional({ values: 'falsy' }).isInt({ min: 0, max: 65535 }), query('map').optional({ values: 'falsy' }).isString().isLength({ max: 40 }), query('region').optional({ values: 'falsy' }).isString().isLength({ max: 80 }), query('sort').optional({ values: 'falsy' }).isIn(['price_asc', 'price_desc', 'recent']), query('limit').optional().isInt({ min: 1, max: 100 }), query('offset').optional().isInt({ min: 0, max: 100000 }), validate, shard.getMarket, ) shardRouter.get( '/market/meta', requireFeature('market'), // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Marketplace size, staleness and filter options' // #swagger.description = 'How many vendors and listings the index holds, how stale it may be (`staleAt` = the oldest vendor row, `freshAt` = the newest), and which facets and regions actually hold vendors — so a client can build its filters without running a search it will discard.' /* #swagger.responses[200] = { description: 'Marketplace metadata', content: { "application/json": { schema: { $ref: "#/components/schemas/UoShardMarketMeta" } } } } */ shard.getMarketMeta, ) shardRouter.get( '/market/vendors/:serial', requireFeature('market'), marketLimiter, // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'One player vendor and everything it is selling' // #swagger.description = 'A single shop by its vendor serial, with its listings. `truncated` (and `total` exceeding `count`) means the shop holds more than the shard publishes per frame — a commodity reseller with thousands of stacks is a real thing, and the page says so rather than presenting a partial shop as complete. Returns 404 for a serial the index has never seen, which also covers a vendor since dismissed or hidden.' /* #swagger.parameters['serial'] = { in: 'path', required: true, description: 'Vendor serial, e.g. 0x40001234', schema: { type: 'string' } } */ // #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Listings to return, 1..500 (default 250).' } // #swagger.parameters['offset'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Listings to skip (default 0).' } /* #swagger.responses[200] = { description: 'The vendor', content: { "application/json": { schema: { $ref: "#/components/schemas/UoShardMarketVendor" } } } } */ /* #swagger.responses[400] = { description: 'Malformed vendor serial' } */ /* #swagger.responses[404] = { description: 'No such vendor in the index' } */ param('serial').isString().isLength({ max: 20 }), query('limit').optional().isInt({ min: 1, max: 500 }), query('offset').optional().isInt({ min: 0, max: 100000 }), validate, shard.getMarketVendor, ) shardRouter.get( '/features', // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Shard features visible to the caller (drives client nav)' // #swagger.description = 'The caller\'s audience rung plus the shard features they may reach, so a client can hide nav entries instead of rendering links that 403. Reports only what the caller can see — the list itself does not disclose gated features.' /* #swagger.responses[200] = { description: 'Visible features', content: { "application/json": { schema: { $ref: "#/components/schemas/UoShardFeatures" } } } } */ shard.getFeatures, ) shardRouter.get( '/stream', // #swagger.tags = ['Public · Shard'] // #swagger.summary = 'Live shard event stream (Server-Sent Events, filtered by audience)' // #swagger.description = 'text/event-stream of live events. The caller\'s audience rung is resolved once at subscribe time and frozen for the connection; each frame is then gated on its feature and field-projected, so sensitive kinds and fields (staff audit, cheat detection, login attempts, IPs, acct/webId) never reach a caller below their configured rung.' /* #swagger.responses[200] = { description: 'An SSE stream (Content-Type: text/event-stream).' } */ shard.stream, ) module.exports = shardRouter