Add shard activity feed + admin live feed; fix public-feed leak
Front ends for the rest of the sidecar data, plus a security fix the live data surfaced. - lib/shardEvents.js: shared describe()/category/label for every event kind (sales, deaths & PvP, skills, fame/karma, quests, world, and staff kinds). - Public /site/shard/activity (ShardActivity): the full event log with category filter tabs and a live tail (history + SSE merged, de-duped). Linked from the Shard page. Shard page now reuses the shared describe(). - Admin: a "Live feed (all events)" panel on the Shard admin page subscribing to the admin SSE channel — shows every kind incl. audit/cheat/login attempts. useShardFeed generalized to take a stream url; api.adminShardStreamUrl added. Security fix: GET /public/shard/feed now restricts to the public-safe kind allowlist (shardEvents.list gains a `kinds` IN-filter). Previously it returned whatever was logged — including audit.* / cheat.* / link.request. Those are still stored for the admin channel but never served publicly (verified: a public request for audit.command returns 0 rows). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
This commit is contained in:
@@ -12,8 +12,18 @@ async function insertIgnore({ kind, t, bootId, payload, dedupeKey }) {
|
||||
return res.affectedRows > 0
|
||||
}
|
||||
|
||||
// Recent events, newest first. Optional kind filter; limit is clamped by the model.
|
||||
async function list({ kind, limit }) {
|
||||
// Recent events, newest first. Filter by a single `kind`, or an allowlist of
|
||||
// `kinds` (IN clause) — the public feed uses the allowlist so it can never leak
|
||||
// staff/sensitive kinds. limit is clamped by the model.
|
||||
async function list({ kind, kinds, limit }) {
|
||||
if (kinds && kinds.length) {
|
||||
const placeholders = kinds.map(() => '?').join(', ')
|
||||
return query(
|
||||
`SELECT id, kind, t, boot_id, payload, created_at
|
||||
FROM shard_events WHERE kind IN (${placeholders}) ORDER BY t DESC LIMIT ?`,
|
||||
[...kinds, limit],
|
||||
)
|
||||
}
|
||||
if (kind) {
|
||||
return query(
|
||||
`SELECT id, kind, t, boot_id, payload, created_at
|
||||
|
||||
@@ -35,9 +35,10 @@ function normalizeLimit(limit) {
|
||||
return Math.min(Math.floor(n), MAX_LIMIT)
|
||||
}
|
||||
|
||||
// Recent events, newest first. Each row's JSON payload is parsed back to an object.
|
||||
async function list({ kind, limit } = {}) {
|
||||
const rows = await db.list({ kind, limit: normalizeLimit(limit) })
|
||||
// Recent events, newest first. Each row's JSON payload is parsed back to an
|
||||
// object. `kinds` (array) restricts to an allowlist; `kind` filters a single kind.
|
||||
async function list({ kind, kinds, limit } = {}) {
|
||||
const rows = await db.list({ kind, kinds, limit: normalizeLimit(limit) })
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
kind: row.kind,
|
||||
|
||||
@@ -47,11 +47,20 @@ async function getStatus(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// GET /public/shard/feed?kind=&limit= — recent notable events from the log.
|
||||
// GET /public/shard/feed?kind=&limit= — recent notable events from the log,
|
||||
// restricted to the public-safe allowlist so staff audit / cheat / link events
|
||||
// (which are stored for the admin channel) can never leak to the public.
|
||||
async function getFeed(req, res) {
|
||||
try {
|
||||
const { kind, limit } = req.query
|
||||
const events = await shardEvents.list({ kind, limit })
|
||||
let events
|
||||
if (kind) {
|
||||
// A specific kind is only served if it is itself public-safe.
|
||||
if (!broadcast.PUBLIC_KINDS.has(kind)) return res.json([])
|
||||
events = await shardEvents.list({ kind, limit })
|
||||
} else {
|
||||
events = await shardEvents.list({ kinds: [...broadcast.PUBLIC_KINDS], limit })
|
||||
}
|
||||
return res.json(events)
|
||||
} catch (err) {
|
||||
log.error('shard.getFeed', err)
|
||||
|
||||
Reference in New Issue
Block a user