docs(shard): record the REST projection gap the Part A smoke test found
The live five-rung smoke test of the visibility framework found that Part
A enforced it on the SSE path and on /guilds + /governors, but not on the
remaining public REST reads - so one event was projected live and served
verbatim from stored history.
link/v3.md gains 3.6.1 with the full list (the anonymous acct/webId leak
on /feed, the flattened ownerAcct on /idoc, the dead `houses` field
rules, /feed ignoring live config, the empty-allowlist fall-through, and
the Date-to-{} projection bug), plus the rule it leaves behind: a read
path that returns shard data and does not project is a bug, and every new
Part B/C surface must gate its kind set on live config rather than on
PUBLIC_KINDS.
3.5 also corrected: the table is NOT seeded on boot. An absent row means
"use the compiled default", which keeps the defaults in one place instead
of duplicating them into a seeder that could drift.
BACKEND_DESIGN.md 6.5 records the same as a security contract: rule 1
locks a field by meaning rather than spelling; PUBLIC_KINDS is a
module-load constant and must not answer per-caller questions;
projectFeature walks arrays and plain objects only.
SHARD_VISIBILITY.md gets the admin-facing version - that stored history
answers the same way the live stream does, and that turning live updates
off stops the push, not the reading.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -711,6 +711,10 @@ rather than silently ignore:
|
||||
1. **`acct` and `webId` are admin-only, always.** They are not exposed as configurable fields, and a
|
||||
stored row attempting to loosen them is discarded on read as well as rejected on write. A character
|
||||
name is visible in game; the account behind it and the website user it links to are not.
|
||||
The lock is on the field's **meaning, not one spelling**: `isLockedField(key)` matches a key that
|
||||
*is* or *ends in* `acct`/`webId`, case-insensitively, so the flattened forms the read models emit
|
||||
(`shapeHouse` → `ownerAcct`, `shapeGuild` → `leaderWebId`) are covered too. An exact-key check was
|
||||
the original implementation and it let `GET /public/shard/idoc` serve `ownerAcct` anonymously.
|
||||
2. **A kind absent from `KIND_FEATURE` is never broadcast below `admin`.** Fail closed. This is what
|
||||
keeps the kind map a security boundary rather than a convenience filter, and it means a shard that
|
||||
starts emitting an unknown event degrades to staff-only, never to public.
|
||||
@@ -731,14 +735,31 @@ it picks, it fails open on one side.)
|
||||
| Nav | `GET /public/shard/features` returns only what the caller may reach, so the SPA never renders a link that would 403. Presentation only. |
|
||||
|
||||
Config reads are cached ~5s, so admin changes take effect within seconds **including on already-open
|
||||
streams**. `PUBLIC_KINDS` still exists and is still exported (`/feed` filtering, `notificationStreams.js`)
|
||||
but is now **derived** from the kind map rather than hand-maintained, so the two cannot drift.
|
||||
streams**. `PUBLIC_KINDS` still exists and is still exported (`notificationStreams.js`) but is now
|
||||
**derived** from the kind map rather than hand-maintained, so the two cannot drift.
|
||||
|
||||
**`PUBLIC_KINDS` is a module-load constant and must not be used to answer "may this caller read this
|
||||
kind?"** — it is computed from the compiled *defaults*, so it cannot see an admin's changes. Use
|
||||
`visibleKinds(level, config)`, which resolves against the live config. `/feed` uses it; it originally
|
||||
used `PUBLIC_KINDS` and consequently kept serving `guild.join` to anonymous callers after an admin had
|
||||
moved `guilds` to `staff`. `visibleKinds` deliberately ignores the `stream` flag: that governs SSE
|
||||
fan-out only, so a feature whose live firehose ships off (market) stays readable from stored history.
|
||||
|
||||
**Every read path that returns shard data must call `projectFeature`.** The stored-history endpoints
|
||||
are not exempt — `/feed` returns the same events the stream does, and returning them unprojected
|
||||
reopens on the REST side exactly what the stream closes. Relatedly, `shardEvents.db.list` treats an
|
||||
**empty** `kinds` array as "serve nothing", never "no filter"; the fall-through it used to take would
|
||||
have turned a fully-gated config into a dump of the entire event log.
|
||||
|
||||
`projectFeature` walks **arrays and plain objects only**. A `Date`, `Buffer` or other class instance
|
||||
is passed through as a value — rebuilding one key-by-key yields `{}`, which is the difference between
|
||||
the pure-JSON wire frames and the DB-backed read models whose rows carry real `Date` columns.
|
||||
|
||||
**Defaults reproduce pre-3.0 behavior exactly**, so installing the framework is a no-op until an admin
|
||||
changes something — with one deliberate exception, which is the leak it was written to close:
|
||||
`/public/shard/guilds` and `/public/shard/governors` previously returned the raw stored payload, whose
|
||||
`leader` / `governor` actors carry `acct` and `webId`. Those fields are now stripped for every caller
|
||||
below admin.
|
||||
changes something — with deliberate exceptions, which are the leaks it was written to close.
|
||||
`/public/shard/guilds`, `/public/shard/governors` and `/public/shard/feed` previously returned the raw
|
||||
stored payload, whose actors carry `acct` and `webId`; `/public/shard/idoc` returned the flattened
|
||||
`ownerAcct`. All are now stripped for every caller below admin.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user