// ── What the bridge can say, and who may hear it ────────────────────────── // // One file, because these two questions have to be answered together or the // second one rots: which frame kinds exist, and which of them a member of the // public may see. // // ── The boundary ────────────────────────────────────────────────────────── // // Protocol 2's catalogue includes frames carrying **IP addresses** (a login // attempt, an approval, a ban) and **one player's complaint about another** (a // report), and one — a destroyed structure — that names where somebody lives. // They are stored, because an operator chasing ban evasion needs them and // because the sidecar persists what it is told. They must never reach a public // page. // // **The boundary is enforced HERE, on the side that serves, and not on the wire.** // The plugin could have stamped a `class` on every frame and saved this file the // trouble; it deliberately does not (PROTOCOL.md §8.5). A boundary declared by // the sender is a boundary a compromised — or merely out-of-date — game host can // widen. Core's own shard fan-out works the same way: a public stream with an // allowlist of kinds, and an admin stream that adds the rest. // // ── Default deny, and why it is not paranoia ────────────────────────────── // // `isPublic` answers `false` for a kind it has never heard of. That matters // because of the shape of the mistake it prevents: the next protocol version // adds a kind, this module ingests it happily (`rust_events` stores what it is // given), and a page that filtered by a DENY list would publish it the day it // first arrived — before anybody had decided whether it should be public. With // an allowlist the new kind is invisible until somebody adds it here, which is // the same moment they think about it. // // The test holds this list against `docs/rust-link/PROTOCOL.md` §8.4's table, so // adding a kind to the spec without classifying it fails a build rather than // shipping an address to a public page. /** * Kinds a public, signed-out visitor may see. * * Each entry is a decision. `player.chat` is here because a shard's chat is * public by the same logic that makes a killfeed public — it happened in front * of everyone who was on the server — and an operator who disagrees turns the * feature off rather than relying on this list being wrong. */ const PUBLIC_KINDS = Object.freeze([ 'player.connected', 'player.disconnected', 'player.respawned', 'player.death', 'player.chat', 'player.tally', 'server.wipe', 'server.initialized', 'server.shutdown', ]) /** * Kinds an admin may see and nobody else. * * Listed rather than implied by absence, so that "we know about this kind and it * is restricted" is distinguishable from "nobody has classified this kind" — the * second is a finding, and a bare allowlist cannot tell you which you are * looking at. */ const STAFF_KINDS = Object.freeze([ 'entity.destroyed', 'player.reported', 'player.banned', 'player.unbanned', 'player.login.attempt', 'player.approved', ]) /** Every kind protocol 2 defines. */ const ALL_KINDS = Object.freeze([...PUBLIC_KINDS, ...STAFF_KINDS]) const PUBLIC = new Set(PUBLIC_KINDS) const STAFF = new Set(STAFF_KINDS) /** * May a signed-out visitor see this kind? * * Default deny: an unknown kind is not public. Callers pass whatever arrived on * the wire, including a kind from a newer protocol this build has never seen. */ function isPublic(kind) { return PUBLIC.has(kind) } /** Is this a kind this build knows about at all? */ function isKnown(kind) { return PUBLIC.has(kind) || STAFF.has(kind) } /** * Narrows a list of requested kinds to the ones a viewer may have. * * Returning the allowlist itself when nothing was requested is what makes the * public route safe by construction rather than by remembering to filter: there * is no code path where "no filter" means "everything". */ function kindsFor({ admin = false, requested = null } = {}) { const permitted = admin ? ALL_KINDS : PUBLIC_KINDS if (!requested || requested.length === 0) return [...permitted] const allowed = new Set(permitted) return requested.filter((k) => allowed.has(k)) } module.exports = { PUBLIC_KINDS, STAFF_KINDS, ALL_KINDS, isPublic, isKnown, kindsFor, }