feat(safety): report, block and hide, MODULE_API 1.21.0
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 36s
PR Checks / server-tests (pull_request) Successful in 6m11s
PR Checks / client-build (pull_request) Successful in 49s

USER_SAFETY.md step 2 (core), with S11-S14 decided while building it.

- Reports: POST /player/reports takes any target type, core's forum
  types or a module's registered ones. One resolver
  (model/safety/targets.js) answers "what is this item, for this
  reader" and 404s an item the reader cannot see. Every report stores
  content_reports.snapshot; the queue shows a module item from it.
- Hide for everyone: POST /admin/moderation/reports/:id/hide hides the
  item through its own moderation (forum hide as staff, or the module's
  hide) and actions the report with actioned_with = 'hide'.
- Blocks (user_blocks) name an item, never a person, and land on the
  author's account first (S12); an unlinked game identity is blocked as
  itself and becomes an account block once linked (S14). Staff cannot
  be blocked; a block on someone later promoted stops applying (S8).
- Hide for me (user_hidden_items), /auth/me/blocks and /auth/me/hidden.
- Forum reads filter blocked authors and hidden items on the server and
  carry canBlock per thread and post.
- MODULE_API 1.21.0: api.registerReportTargets,
  api.registerGameIdentities, ctx.safety.forViewer and
  ctx.safety.staffAmong, so a module filters its own per-viewer feeds on
  the server (S11) and marks lines canBlock (S13).
- Web: Hide for me / Block author in the forum menus, a Blocked section
  on both account pages, Hide for everyone in the Reports queue.
- Tests, OpenAPI, route/guard/engagement manifests regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-30 05:41:00 -05:00
parent 7bd93b8bb0
commit 41a1e84b51
40 changed files with 3974 additions and 56 deletions

View File

@@ -346,6 +346,25 @@ function buildCtx(id, moduleRoot) {
// configuration, so core answers it. A getter, not a captured string: the
// value is read per call, so it cannot go stale against the env.
site: { get baseUrl() { return (process.env.APP_BASE_URL || 'http://localhost:5173').replace(/\/+$/, '') } },
// API 1.21.0 (USER_SAFETY.md §4.1, S11). READ only. What one reader has
// blocked and hidden, so a module filters its own per-reader feeds on the
// server — a chat feed is already read per viewer, so the website and the app
// need no list of their own.
//
// `forViewer` is bound to the CALLING module: it answers the reader's account
// blocks (with any game block whose identity is now linked already folded in,
// S14, and staff dropped, S8), this module's own unlinked game blocks, and
// the reader's hides on this module's report targets. Another module's game
// blocks and hides are not its business. A signed-out reader has none.
//
// `staffAmong(userIds)` answers which of those accounts are staff, for a
// module that marks lines `canBlock` (S13).
safety: {
// eslint-disable-next-line global-require
forViewer: (userId) => require('../model/safety/userSafety.model').forModuleViewer(id, userId),
// eslint-disable-next-line global-require
staffAmong: (userIds) => require('../model/safety/userSafety.model').staffAmong(userIds),
},
// API 1.18.0 (DEMO_MODE.md §3.4, §3.5). True on the public demo. For the
// locks a route list cannot express — a console command by its first word,
// a config file by which plugin owns it — which the module enforces in its
@@ -500,6 +519,18 @@ function buildApi(record) {
once('registerEventTemplates')
record.staged.registerEventTemplates(templates)
},
// API 1.21.0 (USER_SAFETY.md §4.1). What of the module's content a member
// can report, hide for themself and block the author of, and the game
// identity those authors are known by. Both STAGE, and `once` for the reason
// every batch above takes it.
registerReportTargets(targets) {
once('registerReportTargets')
record.staged.registerReportTargets(targets)
},
registerGameIdentities(identities) {
once('registerGameIdentities')
record.staged.registerGameIdentities(identities)
},
// The two lifecycle hooks (§2.5). Registered here, dispatched from
// lifecycle.js — this file runs with no database and the hooks run with one.
// Both are optional: a module with no warm-up and nothing to close simply