docs(safety): record S11-S14 and the user-safety core, MODULE_API 1.21.0
USER_SAFETY.md: S11-S14 (asked while building the core), §3 corrected (the chat feed is already read per viewer), §4 rewritten to match what was built, §7 step 2 recorded as built and walked. MODULE_API.md: 1.21.0 (registerReportTargets, registerGameIdentities, ctx.safety). BACKEND_DESIGN.md: user_blocks / user_hidden_items, the content_reports snapshot and actioned_with, the new routes, the folder map. TEAMS.md §5.6 amended. README row updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -26,13 +26,30 @@ here extends the contract first, in this file, before the module is written agai
|
||||
Core exports a single integer-major semver string from `server/src/modules/version.js`:
|
||||
|
||||
```js
|
||||
const MODULE_API_VERSION = '1.20.0'
|
||||
const MODULE_API_VERSION = '1.21.0'
|
||||
```
|
||||
|
||||
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
|
||||
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
|
||||
module chunk evaluates, which is earlier than any network round trip could answer.
|
||||
|
||||
**1.21.0 — report, block and hide reach a module's content** (`website/USER_SAFETY.md` §4.1,
|
||||
S11-S14). Four optional additions and no removal, so minor.
|
||||
|
||||
- **`api.registerReportTargets([...])`** (§2.4): the types of the module's content a member can
|
||||
report, hide for themself and block the author of, and that staff can hide for everyone from the
|
||||
Reports queue. `snapshot({ targetId, viewer })` answers what the item is **for that viewer**, or
|
||||
`null` when it is gone or they may not see it; core answers 404 for both.
|
||||
- **`api.registerGameIdentities([...])`** (§2.4): the game identity those authors are known by, and
|
||||
`ownersOf(ids)`, which website account each is linked to. A block lands on the account when there
|
||||
is one (S12), and a block on an unlinked identity becomes an account block once it is linked (S14).
|
||||
- **`ctx.safety.forViewer(userId)`** (§2.3): one reader's blocks and hides, so the module filters its
|
||||
own per-viewer feeds on the server (S11). **`ctx.safety.staffAmong(userIds)`**: which accounts are
|
||||
staff, so a module can mark the lines a reader cannot block (S8, S13).
|
||||
- Nothing on `window.__rg` changed: a module's client half calls core's ordinary `/player/reports`,
|
||||
`/auth/me/blocks` and `/auth/me/hidden` through `api.request`. Module-uo's `^1.10.0` still
|
||||
resolves and registers none. Module-Rust's `rust.chat` is the first user.
|
||||
|
||||
**1.20.0 — a module hears that an account is about to be deleted** (`website/ACCOUNT_DELETION.md`
|
||||
§4.1, AD10, AD11). One optional addition and no removal, so minor.
|
||||
|
||||
@@ -51,7 +68,7 @@ module chunk evaluates, which is earlier than any network round trip could answe
|
||||
- **`started`** modules only, like the identity hooks; registered once. The payload is frozen.
|
||||
- Module-uo's `^1.10.0` still resolves and registers none until its own PR: with no hook, a deletion
|
||||
is core's cascade alone, as the admin route's delete always was. `website/USER_SAFETY.md`'s
|
||||
`reportTargets` moves to 1.21.0.
|
||||
report targets moved to 1.21.0.
|
||||
|
||||
**1.19.0 — the public demo refuses a step at save** (`website/DEMO_MODE.md` §3.4, DM18). One optional
|
||||
member on an event action, so minor.
|
||||
@@ -572,6 +589,10 @@ branches lacks — so it is the only place `permits` is true between two values
|
||||
between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise
|
||||
unchanged, and so is every rule about composition narrowing rather than widening.
|
||||
|
||||
**1.21.0 — report, block and hide** (`website/USER_SAFETY.md` §4.1, S11-S14).
|
||||
`api.registerReportTargets`, `api.registerGameIdentities` and `ctx.safety`; four optional additions,
|
||||
so minor; see §1.1. Module-uo's `^1.10.0` still resolves.
|
||||
|
||||
**1.20.0 — account deletion** (`website/ACCOUNT_DELETION.md` §4.1, AD10, AD11). `api.onUserDeleting`,
|
||||
called before an account is deleted so the module unlinks its game links; it cannot veto, and a hook
|
||||
past 10 s is logged and skipped. One optional addition, so minor; see §1.1. Module-uo's `^1.10.0`
|
||||
@@ -904,6 +925,19 @@ module-uo does not need is on the list.
|
||||
| `ctx.inbox.push` | `(userId, item) => void`, fire-and-forget | the in-app channel (live since Phase 7) | `module-uo` reaches both through `server/core.js` (1.7.0) |
|
||||
| `ctx.identities.list` | `(userId, provider) => Promise<[{ provider, subject, displayName, avatarUrl, linkedAt }]>`, read only | `model/userIdentities` | Steam sign-in (1.17.0) |
|
||||
| `ctx.demoMode` | boolean, read once when `ctx` is built | `DEMO_MODE=true` (`demo/mode.js`) | Module-Rust's console-command deny list and config-editor rules (1.18.0) |
|
||||
| `ctx.safety.forViewer` | `(userId) => Promise<{ blockedUserIds, blockedIdentities: { [kind]: [id] }, hidden: { [targetType]: [targetId] } }>`, read only, owner bound | `model/safety/userSafety` | Module-Rust's events feed, filtered per reader (1.21.0) |
|
||||
| `ctx.safety.staffAmong` | `(userIds) => Promise<[userId]>` | `model/safety/userSafety` | a chat line's `canBlock` (1.21.0) |
|
||||
|
||||
**`ctx.safety.forViewer(userId)`** (1.21.0, `USER_SAFETY.md` S11) is what a module's per-viewer feed
|
||||
leaves out for one reader. `blockedUserIds` are the accounts the reader blocks that still apply: a
|
||||
game block whose identity is now linked has already become one (S14), and a block on staff is
|
||||
already gone (S8). `blockedIdentities` holds only **this module's** unlinked game blocks, by kind,
|
||||
and `hidden` only the reader's hides on **this module's** report targets. A signed-out reader
|
||||
(`userId` null) gets empty lists. The rule for a line is: hide it when its speaker's linked account
|
||||
is in `blockedUserIds`; for a speaker with no account, when their id is in `blockedIdentities`; and
|
||||
when its id is in `hidden`. A route that uses it must already be read per viewer (`Cache-Control:
|
||||
private`, varying on `Cookie` and `Authorization`); a shared cache in front of it would serve one
|
||||
reader's filter to another.
|
||||
|
||||
**`ctx.events.emit(triggerId, envelope)`** fires an event the module DECLARED with
|
||||
`api.registerEventTriggers` (§2.4). It is the push half of the engagement seam
|
||||
@@ -1035,6 +1069,8 @@ api.onIdentityLinking(async ({ userId, provider, subject }) => ({ ok: true }))
|
||||
api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => {}) // 1.17.0
|
||||
api.onDemoSeed(async (data, { accounts }) => {}) // 1.18.0
|
||||
api.onUserDeleting(async ({ userId, deletedBy }) => {}) // 1.20.0
|
||||
api.registerReportTargets([{ id, label, snapshot, hide, unhide }]) // 1.21.0
|
||||
api.registerGameIdentities([{ kind, label, ownersOf }]) // 1.21.0
|
||||
```
|
||||
|
||||
**Every call STAGES; nothing is committed until the module as a whole is known good.** A claim's
|
||||
@@ -1573,6 +1609,57 @@ api.onUserDeleting(async ({ userId }) => {
|
||||
the module's to record and retry: core never calls the hook twice for one deletion.
|
||||
- `started` modules only, in registration order; registered once; the payload is frozen.
|
||||
|
||||
**`registerReportTargets([...])`** (1.21.0) — what of the module's content a member can report, hide
|
||||
for themself and block the author of, and that staff can hide for everyone (`USER_SAFETY.md` §4.1).
|
||||
|
||||
```js
|
||||
api.registerReportTargets([{
|
||||
id: 'rust.chat', // namespaced `<moduleId>.`, at most 32 characters
|
||||
label: 'Chat line', // what the Reports queue calls it
|
||||
// What the item is FOR THIS VIEWER, or null when it is gone or they may not see it.
|
||||
async snapshot({ targetId, viewer }) { // viewer: { userId, role }, frozen
|
||||
return {
|
||||
author: { name: 'Rusty', userId: null, identity: { kind: 'steam', id: '7656…' } },
|
||||
text: 'gg ez', at: '2026-10-30T10:00:00Z', where: { server: 'main' },
|
||||
}
|
||||
},
|
||||
async hide({ targetId, actor }) {}, // staff's hide for everyone; actor { id, username, role }
|
||||
async unhide({ targetId, actor }) {},
|
||||
}])
|
||||
```
|
||||
|
||||
- **`snapshot` answers for the viewer.** A `null` and a missing item are one answer to the member
|
||||
(404), so a report, a block or a snapshot never confirms an item they could not have seen (a line
|
||||
on a server whose presence is hidden from them). `author.userId` is the linked website account if
|
||||
there is one; `author.identity` names the game identity, of a kind the module registered with
|
||||
`registerGameIdentities`.
|
||||
- **What core keeps is clipped.** The snapshot is stored on the report, so it outlives the module
|
||||
pruning its own rows. Only `author`, `title`, `text` (500 characters), `at` and up to eight flat
|
||||
`where` fields are kept.
|
||||
- **`hide`** is called from the Reports queue's *Hide for everyone*. The module keeps the hidden
|
||||
flag, leaves the item out for everybody but staff, and shows it to staff marked so they can undo it
|
||||
where it lives (S10).
|
||||
- Each call has core's **5 s**; a module that does not answer is a `503` to the member, never a
|
||||
silent success. `started` modules only; registered once; target ids are integers, as
|
||||
`content_reports.target_id` is.
|
||||
|
||||
**`registerGameIdentities([...])`** (1.21.0) — the identity a module's authors are known by, and
|
||||
which website account each one is linked to (S12, S14).
|
||||
|
||||
```js
|
||||
api.registerGameIdentities([{
|
||||
kind: 'steam',
|
||||
label: 'Steam account',
|
||||
async ownersOf(externalIds) { return { '7656…': 12 } }, // ids with no account are left out
|
||||
}])
|
||||
```
|
||||
|
||||
- A block from one of the module's items lands on the **account** `ownersOf` names; only an id with
|
||||
no account is blocked as itself. A game block is checked again each time it is read, and once its
|
||||
id is linked it is rewritten as an account block, so it then hides the account's forum posts too.
|
||||
- Keyed by module and kind: each module answers only for its own links. A module that does not
|
||||
answer leaves its game blocks as they are, still applying to the id.
|
||||
|
||||
### 2.5 Lifecycle
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user