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
ACCOUNT_DELETION.md phase 2 (core): a member deletes their own account on
the website, and the admin route runs the same procedure.
- DELETE /api/v1/auth/me/account: typed username, password always (AD7),
and a 2FA or recovery code when 2FA is on. A wrong password or code is
400, not 401 (AD12), so the app's refresh-on-401 never replays it.
409 password_required and last_admin; the demo's shared logins are
locked.
- auth/accountDeletion.js: module hooks first, then one transaction that
reserves a staff username (reserved_usernames, AD9) and deletes the row,
then a Teams reconcile, the audit entry (no username) and a notice to
the old address (new protected template auth.account-deleted).
- MODULE_API 1.20.0: api.onUserDeleting({ userId, deletedBy }), before the
delete, 10 s limit, cannot veto (AD10, AD11).
- utils/displayAuthor.js: a deleted account's username snapshots read
"[deleted player]" to members and the stored name, marked, to staff
(AD5, AD6). Used by the forum, the Reports queue, the guest list and
the Team digest. A deleted account drops off Forum guests (AD13).
- Reserved names refuse at every username write, answered as "taken".
- Web: Delete my account on the player and staff Account pages; staff
views mark "(deleted account)".
- OpenAPI regenerated; route manifest and engagement manifest regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
1008 pass (972 before). The tests worth reading first are the ones that pin a
property no screen would look different without:
* **The edit window is decided on the server, twice.** One test proves the read
path stamps `canEdit` per post per viewer; another proves the WRITE path
re-derives it from `created_at` and refuses a stale edit even though the
client was told it could — because a time-bounded permission must not take its
clock from the party it bounds.
* **A locked thread refuses staff too**, asserted over member, leader and staff
in one loop, at 409 rather than 403: well-formed request, refusing state.
* **delete → restore is reversible for images.** Without the second half of the
pair a restored post returns its words and loses its pictures a retention
window later, silently — the test asserts both calls and that `hide` makes
neither.
* **Post moderation recomputes the thread's counters** rather than nudging them;
the test runs hide → unhide → hide, which is the cycle a delta gets wrong.
* **acceptance: nothing in the report model is reachable by a Team leader.** The
negative property is the whole point of §5.6 and negatives are what nobody
notices going, so it is asserted directly — the module's function surface is
pinned, and `queue`/`handle` are checked not to mention leadership at all. If
a leader-facing queue is ever wanted it is the org lead's decision, and this
test is what makes somebody ask.
* **A report never changes the content it is about**, proved by stubbing every
mutation the forum has to throw. If filing a report touched a status then
"report" would BE moderation, and the first person to work that out would have
found a way to hide anything on the site.
The test suite caught one real defect: `describeTarget` returned `undefined` for a
hard-deleted target, and `undefined` is dropped by JSON.stringify — so the
documented `target: null` would have reached clients as an absent key.
Two phase-4 tests were updated rather than added to, both because phase 5 changed
what they describe: `canPost` split into `canPost` (open a discussion, everyone)
and `canAnnounce` (leaders), and `discussion` is no longer a refused thread type.
Phase 5's four new player routes are added to acceptance criterion 2's list, so
"with the forum off every forum route 404s" keeps covering the whole surface.
Co-Authored-By: Claude <noreply@anthropic.com>