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:
44
link/v3.md
44
link/v3.md
@@ -115,9 +115,13 @@ CREATE TABLE IF NOT EXISTS shard_feature_visibility (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
Seeded on boot in `server.js`, one row per feature. **All ten shard features are covered — the four
|
||||
new ones and the six that already ship — and every default reproduces today's behavior, so the
|
||||
retrofit is a no-op until an admin changes something.**
|
||||
**Not** seeded on boot (this changed during implementation): an **absent row means "use the compiled
|
||||
default"**, so the table starts empty and only ever holds rows an admin has actually touched. The
|
||||
defaults live in one place — `FEATURES` in `shardVisibility.js` — instead of being duplicated into a
|
||||
seeder that could drift from it, and a DB blip degrades to those same defaults rather than to
|
||||
"everything is public". **All ten shard features are covered — the four new ones and the six that
|
||||
already ship — and every default reproduces today's behavior, so the retrofit is a no-op until an
|
||||
admin changes something.**
|
||||
|
||||
| Feature | Default audience | Sensitive fields (default rung) |
|
||||
|---|---|---|
|
||||
@@ -156,10 +160,42 @@ Applied at:
|
||||
3. **Nav** — `GET /api/v1/public/shard/features` returns only the features the calling viewer can
|
||||
see, so the SPA hides nav entries rather than rendering links that 403.
|
||||
|
||||
### 3.6.1 What the first implementation missed (found by the §11 smoke test, fixed)
|
||||
|
||||
Part A shipped enforcement on the SSE path and on `/guilds` + `/governors`, but the **remaining public
|
||||
REST reads never called into it** — so the same event was projected live and served verbatim from
|
||||
history. Recorded because each miss is a shape the next phase can repeat:
|
||||
|
||||
- **`/public/shard/feed` returned the stored payload as-is.** `actor.acct` / `actor.webId` were
|
||||
readable *anonymously* for every logged kind (`player.death`, `mob.killed`, `skill.gain`,
|
||||
`guild.join`, …) — broader than the §3.1 leak, which was limited to board holders.
|
||||
- **`/public/shard/idoc` returned `ownerAcct`.** Rule 1 keyed on the exact strings `acct`/`webId`,
|
||||
but `shapeHouse` flattens the actor into `ownerAcct` / `ownerName` / `ownerSerial`. The lock is now
|
||||
on the field's **meaning** — a key that is or ends in `acct`/`webId`, case-insensitively — so
|
||||
flattened spellings are covered and unwritten shapes fail closed.
|
||||
- **The `houses` field rules were dead config.** Neither `getIdoc` nor `getHouses` projected, so the
|
||||
panel offered toggles that did nothing. **Every feature's declared fields must name the keys the
|
||||
read model actually emits**, not just the wire frame's.
|
||||
- **`/feed` filtered on `PUBLIC_KINDS`**, a module-load constant derived from the compiled defaults,
|
||||
so live audience changes never reached it. `visibleKinds(level, config)` resolves the readable set
|
||||
from live config; it deliberately ignores the `stream` flag, which governs SSE fan-out only (market
|
||||
history stays readable with its firehose off).
|
||||
- **`shardEvents.db.list` treated an empty `kinds` array as "no filter"** and fell through to an
|
||||
unfiltered `SELECT`. A fully-gated config would have dumped the whole event log, staff audit
|
||||
included. An empty allowlist now serves nothing.
|
||||
- **`projectValue` recursed into every object**, so a `Date` column came back as `{}`. It walks
|
||||
arrays and plain objects only. The unit tests used JSON fixtures and could not have caught this —
|
||||
the live read did, which is the argument for §11's smoke test over tests alone.
|
||||
|
||||
**The rule this leaves behind:** *a read path that returns shard data and does not call
|
||||
`projectFeature` is a bug.* Every new surface in Parts B and C — `/ruleset`, `/points`, `/market`,
|
||||
`/atlas` — must project, and must gate its kind set on live config rather than on `PUBLIC_KINDS`.
|
||||
|
||||
### 3.7 Admin surface
|
||||
|
||||
`GET` / `PUT /api/v1/admin/shard/visibility` (admin-only). Validate feature names against the known
|
||||
set and rungs against the ladder; reject any attempt to set `acct`/`webId` below `admin`. Writes an
|
||||
set and rungs against the ladder; reject any attempt to set a locked field below `admin` — including
|
||||
its flattened spellings (`ownerAcct`, `leaderWebId`), see §3.6.1. Writes an
|
||||
`admin.audit`-style row so visibility changes are traceable. New client panel
|
||||
`routes/admin/ShardVisibility.jsx` at `/admin/shard-visibility`, linked from `ShardAdmin.jsx`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user