// 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 express = require('express') const { param, query } = require('express-validator') const shard = require('./shard.controller') const validate = require('../../../middleware/validate') 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/ShardStatus" } } } } */ 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/ShardEvent" } } } } } */ 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/ShardEconomyPoint" } } } } } */ 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/ShardOnlinePlayer" } } } } } */ 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/ShardHouse" } } } } } */ 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/ShardHouse" } } } } } */ 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( '/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/ShardFeatures" } } } } */ 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