From 6f95ed09d049bf543f3acb6d683dadca0e12e14f Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 29 Oct 2026 08:21:09 -0500 Subject: [PATCH 1/5] docs(android): record M8 step 2, the data-safety audit runicgateway.com#44 declares everything the app sends as collected, not shared, encrypted in transit and required, after an audit of data/api/ on edge. Account deletion is left open for the org lead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- android/PLAN.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/android/PLAN.md b/android/PLAN.md index a24f1d0..07fbf9e 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1042,7 +1042,17 @@ push, and Play (M6–M8) follow the designed app. `s22_ultra` emulator against a local site whose relay was `ntfy.sh`: the service ran as `specialUse` (`types=0x40000000`), a tickle arrived, and push came back by itself after a reboot and after an update. After a sign-out and a reboot it stayed off. - 2. **Data safety** (runicgateway.com): the audit and the generator. One PR. + 2. **Data safety** (runicgateway.com): the audit and the generator. One PR. **Built + 2026-10-29** as `runicgateway.com#44` (its D84). The audit read every request body, query + and header in `data/api/` on `edge` and found more leaving the phone than the old notes + listed: the sign-in, the phone's make and model on a trusted-device request, the + User-Agent, game-account links and in-app game-account creation, the Contact form, staff + posts and moderation, notification read state and preferences, and search text. All of it + is declared collected, not shared, encrypted in transit, required. The server address + and the inbox snapshot stay on the phone. Step 3 adds its own rows when it lands. + **Open:** the form's account-deletion questions. The app starts accounts (the sign-up + link to the deployment's website, and in-app game-account creation), and the platform has + no self-service account deletion. 3. **Report, block and hide** across `website`, `Module-Rust`, the kit and this app (`USER_SAFETY.md` §7). 4. **The cutover**, `edge` → `main`, once 1-3 have landed, so the first Play bundle carries -- 2.49.1 From 988c0000741507603819657de1c8efbff76b1867 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 29 Oct 2026 17:08:39 -0500 Subject: [PATCH 2/5] docs(android): record the account-deletion decision for M8 The org lead chose self-service deletion on the website and in the app, with a runicgateway.com page as Play's deletion URL. Planned separately, before the cutover. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- android/PLAN.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/android/PLAN.md b/android/PLAN.md index 07fbf9e..77a70cd 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1050,9 +1050,13 @@ push, and Play (M6–M8) follow the designed app. posts and moderation, notification read state and preferences, and search text. All of it is declared collected, not shared, encrypted in transit, required. The server address and the inbox snapshot stay on the phone. Step 3 adds its own rows when it lands. - **Open:** the form's account-deletion questions. The app starts accounts (the sign-up - link to the deployment's website, and in-app game-account creation), and the platform has - no self-service account deletion. + **Account deletion, decided (org lead, 2026-10-29): build self-service deletion.** The + app starts accounts (the sign-up link to the deployment's website, and in-app + game-account creation), so Play requires a deletion path in the app and at a web address, + and the platform had none: only an admin could delete an account. A member gets *Delete + my account* on the website and in the app, which removes the account and its game links. + A runicgateway.com page explains it, and that page's address is the one Play is given. It + is planned as its own piece of work, before the cutover. 3. **Report, block and hide** across `website`, `Module-Rust`, the kit and this app (`USER_SAFETY.md` §7). 4. **The cutover**, `edge` → `main`, once 1-3 have landed, so the first Play bundle carries -- 2.49.1 From 2ab35334d6e98564fb3fe02808ee07cc2f18f2f4 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 29 Oct 2026 17:18:02 -0500 Subject: [PATCH 3/5] docs(website): plan self-service account deletion ACCOUNT_DELETION.md is the plan of record for the org lead's decision: a member deletes their own account on the website and in the app, as Google Play requires of an app that starts accounts. - One deletion service for the admin route and the member's own route, with a confirm step, the last-admin and demo refusals, clearing the username snapshots on what the member wrote, and a notice by email. - MODULE_API 1.20.0: api.onUserDeleted, which runs after the commit and can't stop a deletion. - Screens on the web and in the app; runicgateway.com /delete-account/ as the address Play is given. - What the schema already does (59 keys: credentials cascade, content detaches), the gaps, six questions, and the build order. M8's order puts account deletion before report, block and hide, which moves USER_SAFETY.md's reportTargets to MODULE_API 1.21.0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- README.md | 3 +- android/PLAN.md | 13 +- website/ACCOUNT_DELETION.md | 268 ++++++++++++++++++++++++++++++++++++ website/USER_SAFETY.md | 7 +- 4 files changed, 282 insertions(+), 9 deletions(-) create mode 100644 website/ACCOUNT_DELETION.md diff --git a/README.md b/README.md index 924c5db..90843ed 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ sidecar as a service, and hands you the values the website needs. | [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework | | [TRUSTED_DEVICES_MFA.md](website/TRUSTED_DEVICES_MFA.md) | TOTP two-factor, trusted devices and recovery codes | | [STEAM_SIGNIN.md](website/STEAM_SIGNIN.md) | **Plan, built 2026-10-19** — Sign in with Steam as a built-in provider beside Google and Discord (OpenID 2.0), and a Steam id for modules | -| [USER_SAFETY.md](website/USER_SAFETY.md) | **Plan, 2026-10-29** — report, block and hide on the website and in the app: an account-wide block, a member's own hide and staff's hide for everyone, with module-owned report targets (`MODULE_API` 1.20.0) | +| [ACCOUNT_DELETION.md](website/ACCOUNT_DELETION.md) | **Plan, 2026-10-29**: a member deletes their own account on the website and in the app, as Google Play requires. One deletion service for staff and self, an `onUserDeleted` module hook (`MODULE_API` 1.20.0), and runicgateway.com's `/delete-account/` page as the address Play is given | +| [USER_SAFETY.md](website/USER_SAFETY.md) | **Plan, 2026-10-29** — report, block and hide on the website and in the app: an account-wide block, a member's own hide and staff's hide for everyone, with module-owned report targets (`MODULE_API` 1.21.0, after account deletion) | | [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, fully decided 2026-10-23** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | | [MODERATION_APPEALS.md](website/MODERATION_APPEALS.md) | Moderation actions, content reports and the appeals flow | | [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from the shard's own ServUO files — served over the bridge since Protocol 8, so no shared filesystem | diff --git a/android/PLAN.md b/android/PLAN.md index 77a70cd..44d316a 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1055,11 +1055,14 @@ push, and Play (M6–M8) follow the designed app. game-account creation), so Play requires a deletion path in the app and at a web address, and the platform had none: only an admin could delete an account. A member gets *Delete my account* on the website and in the app, which removes the account and its game links. - A runicgateway.com page explains it, and that page's address is the one Play is given. It - is planned as its own piece of work, before the cutover. - 3. **Report, block and hide** across `website`, `Module-Rust`, the kit and this app - (`USER_SAFETY.md` §7). - 4. **The cutover**, `edge` → `main`, once 1-3 have landed, so the first Play bundle carries + A runicgateway.com page explains it, and that page's address is the one Play is given. The plan is + [`website/ACCOUNT_DELETION.md`](../website/ACCOUNT_DELETION.md), step 3 below. + 3. **Account deletion** across `website`, the two modules, the kit, this app and + runicgateway.com (`../website/ACCOUNT_DELETION.md` §6). First, because Play requires it + and it takes `MODULE_API` 1.20.0 (org lead, 2026-10-29). + 4. **Report, block and hide** across `website`, `Module-Rust`, the kit and this app + (`USER_SAFETY.md` §7), now `MODULE_API` 1.21.0. + 5. **The cutover**, `edge` → `main`, once 1-4 have landed, so the first Play bundle carries all of it. **Before it merges, the org lead enrolls the existing keystore as the app-signing key** in Play Console (App integrity, "use my own key"). `PLAY_SERVICE_ACCOUNT_JSON` is already set, so the release run will call Play, and the signing choice is permanent. On the diff --git a/website/ACCOUNT_DELETION.md b/website/ACCOUNT_DELETION.md new file mode 100644 index 0000000..1bd194d --- /dev/null +++ b/website/ACCOUNT_DELETION.md @@ -0,0 +1,268 @@ +# Account deletion — a member deletes their own account, on the website and in the app + +**Status:** plan of record, 2026-10-29. Nothing built yet. §5 holds the questions for the org lead; +each phase in §6 stops for the org lead before the next one starts. + +**Why now.** Google Play's Data safety form asks whether the app lets people create an account, and +an app that does must let them delete it, both **in the app** and at a **web address** given to Play +(`android/PLAN.md` M8, step 2). The app starts accounts in two places: the sign-in screen's link to +the deployment's sign-up page, and in-app game-account creation where a game server offers it +(`PlayerShardApi.createAccount`). The platform had no self-service deletion: only an admin could +delete an account. The org lead chose to build it (AD1) and to do it **before** report, block and +hide (`USER_SAFETY.md`), so it takes the next `MODULE_API` minor. + +## 1. What a member can do + +| Where | What they see | What happens | +|---|---|---| +| Website, *Account* (`PlayerAccount.jsx`) | a *Delete my account* section at the bottom: what goes, what stays (§4.1), and a confirm step | the account is deleted (or scheduled, §5 Q1), the member is signed out everywhere, and the site says so | +| App, *Account* (`AccountScreen.kt`) | the same section and the same confirm step | the same, and the app clears what it holds on the phone, as a sign-out does | +| runicgateway.com `/delete-account/` | how to do it on the website or in the app, what is deleted and what is kept, and who to ask when you cannot sign in | nothing; it is the page whose address Play is given (AD2) | + +The confirm step asks for the account's password, and for a two-factor code when the account has +two-factor sign-in on. An account with neither is §5 Q4. It also asks the member to type their +username, so a mis-tap on a phone cannot delete an account. + +Every member can delete themself, staff included. The one refusal is the **last admin**, as the +admin route already refuses. Then a deployment would be left with no one who can run it. + +## 2. Decisions (org lead, 2026-10-29) + +- **AD1 — Build self-service deletion.** The org lead chose this over an explanation page alone, + and over removing the app's account-creation links. A member deletes their account on the + website and in the app, and that removes the account and its game links. +- **AD2 — runicgateway.com hosts the deletion page Play is given.** We publish the app, so + Play needs one address from us. Every deployment runs its own site, so that page explains the + steps rather than performing them. +- **AD3 — Before report, block and hide.** It takes `MODULE_API` **1.20.0**, and + `USER_SAFETY.md`'s `reportTargets` moves to 1.21.0. + +## 3. What exists today (read 2026-10-29, website `main` 222bcb0) + +- **The admin route.** `DELETE /api/v1/admin/users/:id` refuses your own account and the last admin, + then `users.remove(id)`, a plain `DELETE`. Everything else is the schema's foreign keys. +- **What the foreign keys already do.** Of the 59 keys that reference `users(id)`: + - **CASCADE** removes what belongs to the account alone: linked sign-in identities, mobile + refresh tokens and SSO codes, revoked sessions, trusted devices, recovery codes, password + resets, email verifications, push devices, notification subscriptions and channel prefs, + in-app notifications, and the engagement outbox, cooldowns and digest state. + - **SET NULL** detaches what the member made from them: news posts, wiki edits, forum threads, + posts and uploads, reports filed, appeals, Team activity, event participation, engagement + send logs, and the activity log. + - Modules: Module-uo's `shard_account_links`, and Module-Rust's `rust_account_links`, + `rust_perm_group_members`, `rust_perm_grants`, `rust_perm_run_grants` and + `rust_permgroup_members`, all **CASCADE**. Module-Rust's grants and group memberships keyed + straight to a Steam id (`rust_perm_steam_grants`, `rust_permgroup_steam_members`) have no + site account behind them, and stay. Staff gave them to a player in the game, not to a member + of the site. +- **Sessions die at once.** `requireAuth` re-reads the user on every request and answers 401 when + the row is gone (`session.middleware.js`). So a deleted member's web cookie or mobile access + token stops working on its next use, without waiting for it to expire. +- **Rust permissions are withdrawn in the game by themselves.** `permSync.js` compares a digest of + the desired set with what each server last confirmed. When the grant rows cascade away, the digest + changes, and the next tick pushes the set with the withdrawn grants. +- **Teams follow on the next reconcile.** `team_members.user_id` is SET NULL. The roster belongs to + the module and re-syncs every 15 minutes (`TEAMS.md`). + +**The gaps:** +1. There is no self-service route, and no screen on the web or in the app. +2. **Their username survives in snapshot columns.** The Teams tables store a username next to the + user id, deliberately, so a record still names someone after the account is gone. + `teamForum.model.js` shows "[deleted account]" only when the snapshot is empty too. There are + two kinds: + - **What they wrote:** `team_forum_threads.created_username`, + `team_forum_posts.author_username`, `team_forum_uploads.uploader_username`. + - **Records of what was done:** + - `content_reports.reporter_username` and `handled_username` + - `team_forum_moderation.actor_username` + - `team_forum_grants.username`, `granted_username` and `revoked_username` + - `team_leader_overrides.actor_username` + - `team_moderation_requests.requested_username` and `decided_username` +3. **The activity log keeps IP addresses.** Rows for the deleted member keep `ip`, and `detail` + can carry a username, with only `user_id` cleared (§5 Q3). +4. **A module cannot learn that a user was deleted.** If a module keeps user data without a + foreign key, or has a side effect in the game, it has no hook. The Module API has + `onIdentityLinked`, but nothing for deletion. +5. **A game account the app created stays on the game server.** For UO the link cascades, but the + shard account and its characters remain (§5 Q5). +6. The demo's four accounts (`demo/mode.js`) must not be deletable. + +## 4. Design + +### 4.1 Core (`website`) + +- **One service, two routes.** A new `accountDeletion.deleteAccount(userId, { req, by })` holds + the whole procedure. The admin route's `users.remove(id)` call is replaced by it, so a deletion + by staff and a deletion by the member do the same thing. +- **The member's route.** `DELETE /api/v1/auth/me/account`, behind `requireAuth` and + `accountChangeLimiter`. + - Body: `{ username, password?, code?, recoveryCode? }`. + - The typed `username` must match the account, ignoring case. + - The password is checked when the account has one. + - A two-factor code or a recovery code is checked when two-factor sign-in is on. + - Refusals: + - 400 for a username that doesn't match + - 401 for a wrong password or code + - 409 for the last admin + - the demo's own refusal for a demo account (`demo/mode.js`, the same lock other breaking + settings use) + - `#swagger.*` annotations; the spec is regenerated. +- **The procedure, in order:** + 1. **Before the delete**, read what is needed afterwards: the email address (for the notice + below) and the linked game ids. + 2. **In one transaction:** + - clear the "what they wrote" snapshots (gap 2), so their threads, posts and uploads show + "[deleted account]"; §5 Q2 decides whether the content itself stays + - apply §5 Q3's answer to the "records" snapshots and to their activity-log rows + - delete the `users` row, so the foreign keys do the rest + 3. **After commit**, fire-and-forget, each in its own `try`: + - every module's `onUserDeleted` hook (below) + - `teams.reconcile({ reason: 'account deleted' })` + - an activity-log entry, `user.delete` (by staff) or `user.self_delete`, naming the id and + never the username + - an email to the address read in step 1, when there was one and mail is configured: + "your account on was deleted on ". This is the only notice a person gets + if someone else used their unlocked phone, and it says who to contact. + - **Nothing can stop a deletion once it starts.** A module hook that throws is logged and the + deletion still stands, because Play requires the request to be honoured. That is also why the + hook runs after the commit rather than before it. +- **`MODULE_API` 1.20.0: `api.onUserDeleted(async ({ userId, deletedBy }) => {})`**, where + `deletedBy` is `'self' | 'staff'`. + - It runs after the row is gone, so a module that keeps data without a foreign key cleans it + up, and a module with game-side effects acts on them at once. It must not throw. + - It is the same staging and validation as `onIdentityLinked`. + - `MODULE_API.md` §2.4 and §2.5 are amended. The Integration Kit's `coreApi` check goes red + until it is re-pinned, which is how the kit is meant to notice. +- **What goes and what stays** is written once in `BACKEND_DESIGN.md`, and the screens and the + runicgateway.com page say the same in plain words: + - **Goes:** the account, password, email address, two-factor secret and recovery codes; every + session, trusted device and push registration; linked Google, Discord, OIDC and Steam sign-ins; + game-account links and, in Rust, the permissions and groups the site gave those Steam ids; + notification settings and the inbox. + - **Stays, without your name:** what you wrote where others read it (§5 Q2), shown as + "[deleted account]"; reports you filed and appeals you made, as moderation records; event + results, with the score kept and the name removed; the delivery log's hashes and the + suppression list (`collection.mjs` already says so). + - **Stays for security:** §5 Q3's answer. +- **Docs:** `BACKEND_DESIGN.md` (route, procedure, the table above), `MODULE_API.md`, OpenAPI. + +### 4.2 Website client + +- **`PlayerAccount.jsx`** gets a *Delete my account* section, last on the page and visually set + apart. It holds the "what goes, what stays" text and a button that opens a confirm dialog with + the username, password and (if needed) code fields. *Delete* stays disabled until the username + matches. +- **On success**, the client clears its session state and lands on the home page with a notice: + "Your account has been deleted." +- **On the demo** the section shows, and its button is disabled with the demo's usual locked + wording, so a reviewer can see the feature exists. + +### 4.3 Android app (`edge`) + +- **`AccountScreen.kt`** gets a *Delete my account* row that opens a new screen with the same text + and fields, and `MeApi.deleteAccount(body)`. +- **On success** the app does what a sign-out does, without the logout call, because the server + has already dropped everything. It clears the tokens, the trusted-device token, the push + registration and the inbox snapshot, then returns to sign-in with "Your account has been + deleted." +- **On the demo** the server refuses, and `demoLockInterceptor` shows the usual locked message. + The row is still visible, which is what Play's reviewer needs to find. + +### 4.4 Modules + +- **Module-Rust (`edge`):** + - `onUserDeleted` marks the permission sync dirty, so withdrawn grants reach the game on the + next tick rather than waiting for the digest check. + - The links and grants themselves already cascade. + - Core-ref pin. +- **Module-uo:** + - `shard_account_links` already cascades. + - `onUserDeleted` does whatever §5 Q5 decides for the shard account. + - Core-ref pin. +- **Integration Kit:** the re-pin, and a paragraph in the module chapter: what `onUserDeleted` is + for, and that a module keeping user data with no foreign key must use it. + +### 4.5 runicgateway.com + +- **`/delete-account/`**, the page Play is given (AD2). It covers: + - the app's name and ours + - the steps on the website and in the app + - what is deleted and what is kept, and for how long + - that we hold no account, because each community runs its own site + - what to do if you cannot sign in: ask that community's operator, who can delete the account + from the admin panel +- **The data, written once.** The page's facts become `deployment` rows in `collection.mjs`, so + `/privacy` and the page agree. `PLAY_DATA_SAFETY.md`'s account-deletion bullet then names the URL + and answers "yes" to "can users request deletion". + +## 5. Questions for the org lead + +- **Q1 — Delete now, or after a grace period?** + - *Example:* a player taps *Delete* at 2 a.m. after a bad raid. + - **Now:** the account is gone, and the username is free to register by morning. + - **Grace period (14 days):** the account is locked at once and signs them out everywhere. + Signing in during the 14 days cancels the deletion, and a nightly sweep deletes it when the + period ends. That needs a `pending_deletion` status and a sweep. + - *Recommended: now.* It is simpler, it is what the confirm step is for, and Play accepts either. +- **Q2 — What happens to what they wrote?** + - *Example:* a member wrote a forum guide with 40 replies under it. + - **Keep it, shown as "[deleted account]":** the thread still reads, with no name on it. + - **Delete their posts and uploads:** the guide is gone, and the replies answer nothing. + - *Recommended: keep it, anonymous.* Most forums work this way, and the screens say so before + the member confirms. +- **Q3 — Records of what was done: the activity log's IP addresses, and the moderation + snapshots.** + - *Example:* a banned player deletes their account and comes back as someone new. Or a Team + leader who hid a post deletes their account, and staff later ask who hid it. + - **Keep them as they are:** moderators can still match the old IPs, and a record still says + who filed the report or hid the post. Those are security and moderation records, and the + privacy notes say so. + - **Clear them:** clear `ip` and the username in `detail` in the activity log, and the + "records" snapshots in gap 2. Nothing can be matched any more, and those records then read + "[deleted account]". + - *Recommended: keep them,* as security and moderation records kept until the operator removes + them, which is what `collection.mjs` already says about the audit log. +- **Q4 — How does an account with no password confirm?** + - *Example:* a player who only ever signs in with Discord, with no two-factor sign-in. + - **Typing the username on a signed-in session is enough:** simple, but a person holding their + unlocked phone can do it. + - **Sign in with the provider again first** (within the last 10 minutes): safer, but it needs a + re-authentication flow on the web and in the app. + - *Recommended: sign in again,* reusing the SSO flow with a `reauth` purpose. +- **Q5 — The UO game account the app can create.** + - *Example:* a player made `Thorne` on the shard from the app and has three characters on it. + - **Leave it on the shard:** the link goes, and the shard account and characters stay. The + shard's staff deal with it as they would any account. + - **Offer to delete it too:** the confirm step adds "Also delete game account Thorne and its + characters". That needs a new bridge command, `account.delete`, and a protocol bump in + servuo-plugins, link and module-uo. + - *Recommended: leave it.* Play's rule concerns the account a user signs in to the app with, + which is the site account, and a shard account belongs to the shard. If Play's review asks, + the second option can follow. +- **Q6 — Can someone else take the username straight away?** + - *Example:* a moderator called `Ravenna` deletes their account, and the next day someone + registers `Ravenna` and messages players as staff. + - **Free at once:** simple. + - **Reserved for 90 days:** a small `reserved_usernames` table that registration and username + changes check. + - *Recommended: reserve for 90 days*, for staff accounts only. + +## 6. Build order + +1. **This plan** (docs), with §5's answers. +2. **Core** (`website`): §4.1 and §4.2, `MODULE_API` 1.20.0, tests (the service's procedure, the + route's refusals, the demo lock, a module hook that throws), OpenAPI, `BACKEND_DESIGN.md`, + `MODULE_API.md`. One PR. +3. **Module-Rust** (`edge`) and **Module-uo**: §4.4 plus their core-ref pins, one PR each. + **Integration Kit**: the re-pin and the paragraph, one PR. +4. **Android** (`edge`): §4.3. One PR. +5. **runicgateway.com**: §4.5, plus `PLAY_DATA_SAFETY.md` regenerated. One PR. +6. **A walk**, using a throwaway account on each surface: + - delete on the website, and in the app on the emulator + - the other device's session dies on its next request + - a Rust-linked account's grants leave the rig on the next sync + - a forum post shows "[deleted account]" + - the demo refuses + - the last admin is refused +7. Then `USER_SAFETY.md` (report, block and hide, now 1.21.0), then `android/PLAN.md` M8's + cutover. diff --git a/website/USER_SAFETY.md b/website/USER_SAFETY.md index cb9a8c9..7d2cd8f 100644 --- a/website/USER_SAFETY.md +++ b/website/USER_SAFETY.md @@ -1,7 +1,8 @@ # Report, block and hide — user safety on the website and in the app **Status:** plan of record, 2026-10-29. Nothing built yet. Each phase stops for the org lead -before the next one starts. +before the next one starts. Built after account deletion ([`ACCOUNT_DELETION.md`](ACCOUNT_DELETION.md)), +which is why it is `MODULE_API` 1.21.0. **Why now.** Google Play's content-rating questionnaire for the Android app (`android/PLAN.md` M8) was answered "users or user-generated content can be reported". The app shows other players' chat @@ -56,7 +57,7 @@ staff's. a module's content can be reported as (for Rust: `rust.chat`). Each type answers two questions for core: `snapshot(id)` (author, text, time, where it was said), and `hide(id, actor)` / `unhide(id, actor)`. Core files the report, stores the snapshot and runs the queue. The module owns - the content and its hidden flag. This is a `MODULE_API` minor bump, **1.20.0**. The Integration + the content and its hidden flag. This is a `MODULE_API` minor bump, **1.21.0**. The Integration Kit re-pins and gains a paragraph. - **One filing route for every target**: `POST /player/reports {targetType, targetId, reason, detail?}`. Forum types keep working through the existing forum route, which then calls the same @@ -119,7 +120,7 @@ Play declarations gain "user-generated content: collected". M21 says so. ## 7. Build order 1. **This plan** (docs), with §5's answers. -2. **Core** (`website`): §4.1, `MODULE_API` 1.20.0, tests, OpenAPI, `BACKEND_DESIGN.md`, +2. **Core** (`website`): §4.1, `MODULE_API` 1.21.0, tests, OpenAPI, `BACKEND_DESIGN.md`, `MODULE_API.md`, `TEAMS.md` §5.6 amended. One PR. 3. **Module-Rust** (`edge`): §4.2 plus the core-ref pin. One PR. **Integration-kit**: the re-pin and a paragraph on `reportTargets`. One PR. -- 2.49.1 From 710262a59130d854a837e4952731f7b8551875fe Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 29 Oct 2026 17:23:58 -0500 Subject: [PATCH 4/5] docs(website): record the account-deletion answers (AD4-AD9) The org lead answered the plan's six questions on 2026-10-29: - delete at once, no grace period - what a member wrote stays, and members see every snapshot of a deleted account as "[deleted player]" - staff keep the name and the IP for moderation, so the stored rows are untouched and the rule is about who sees what (displayAuthor) - the password is always the proof; SSO sits on top of a real account - a UO game account stays on the shard; no protocol change - a deleted staff username is never available again (reserved_usernames) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- website/ACCOUNT_DELETION.md | 167 +++++++++++++++++++----------------- 1 file changed, 88 insertions(+), 79 deletions(-) diff --git a/website/ACCOUNT_DELETION.md b/website/ACCOUNT_DELETION.md index 1bd194d..fef2808 100644 --- a/website/ACCOUNT_DELETION.md +++ b/website/ACCOUNT_DELETION.md @@ -1,7 +1,7 @@ # Account deletion — a member deletes their own account, on the website and in the app -**Status:** plan of record, 2026-10-29. Nothing built yet. §5 holds the questions for the org lead; -each phase in §6 stops for the org lead before the next one starts. +**Status:** plan of record, 2026-10-29, with every question answered (AD4-AD9). Nothing built +yet. Each phase in §6 stops for the org lead before the next one starts. **Why now.** Google Play's Data safety form asks whether the app lets people create an account, and an app that does must let them delete it, both **in the app** and at a **web address** given to Play @@ -15,13 +15,14 @@ hide (`USER_SAFETY.md`), so it takes the next `MODULE_API` minor. | Where | What they see | What happens | |---|---|---| -| Website, *Account* (`PlayerAccount.jsx`) | a *Delete my account* section at the bottom: what goes, what stays (§4.1), and a confirm step | the account is deleted (or scheduled, §5 Q1), the member is signed out everywhere, and the site says so | +| Website, *Account* (`PlayerAccount.jsx`) | a *Delete my account* section at the bottom: what goes, what stays (§4.1), and a confirm step | the account is deleted at once (AD4), the member is signed out everywhere, and the site says so | | App, *Account* (`AccountScreen.kt`) | the same section and the same confirm step | the same, and the app clears what it holds on the phone, as a sign-out does | | runicgateway.com `/delete-account/` | how to do it on the website or in the app, what is deleted and what is kept, and who to ask when you cannot sign in | nothing; it is the page whose address Play is given (AD2) | The confirm step asks for the account's password, and for a two-factor code when the account has -two-factor sign-in on. An account with neither is §5 Q4. It also asks the member to type their -username, so a mis-tap on a phone cannot delete an account. +two-factor sign-in on. It also asks the member to type their username, so a mis-tap on a phone +cannot delete an account. Signing in with Google, Discord or Steam is a convenience on top of an +account that already has its own password (AD7), so the password is always the proof. Every member can delete themself, staff included. The one refusal is the **last admin**, as the admin route already refuses. Then a deployment would be left with no one who can run it. @@ -37,6 +38,26 @@ admin route already refuses. Then a deployment would be left with no one who can - **AD3 — Before report, block and hide.** It takes `MODULE_API` **1.20.0**, and `USER_SAFETY.md`'s `reportTargets` moves to 1.21.0. +The org lead's answers to the six questions in §5, the same day: + +- **AD4 — Delete now** (Q1). There is no grace period. The typed username and the password are + the safeguard. +- **AD5 — What they wrote stays, attributed to "[deleted player]"** (Q2). For members, every + username snapshot of a deleted account reads "[deleted player]": their threads, posts and + uploads, and the records in gap 2. That label replaces today's "[deleted account]". +- **AD6 — Staff keep the name and the IP** (Q3). The snapshots and the activity log's IP + addresses stay in the database, and the staff views keep showing them, marked as a deleted + account, for moderation. So AD5 is a rule about what members see, not a change to the stored + rows. +- **AD7 — The password is the proof** (Q4). SSO is a convenience. A member must already have a + proper account before they can link a sign-in provider, so every account has its own password, + and the confirm step always asks for it. An account that somehow has no password is told to set + one first, on the same Account page. +- **AD8 — A UO game account stays on the shard** (Q5). Only the link goes. The shard's staff + handle the shard account like any other. There is no protocol change. +- **AD9 — A deleted staff account's username is never available again** (Q6). It can never be + registered, or taken by a username change. A player's username is free at once. + ## 3. What exists today (read 2026-10-29, website `main` 222bcb0) - **The admin route.** `DELETE /api/v1/admin/users/:id` refuses your own account and the last admin, @@ -79,13 +100,15 @@ admin route already refuses. Then a deployment would be left with no one who can - `team_leader_overrides.actor_username` - `team_moderation_requests.requested_username` and `decided_username` 3. **The activity log keeps IP addresses.** Rows for the deleted member keep `ip`, and `detail` - can carry a username, with only `user_id` cleared (§5 Q3). + can carry a username, with only `user_id` cleared. They stay, for staff (AD6). 4. **A module cannot learn that a user was deleted.** If a module keeps user data without a foreign key, or has a side effect in the game, it has no hook. The Module API has `onIdentityLinked`, but nothing for deletion. 5. **A game account the app created stays on the game server.** For UO the link cascades, but the - shard account and its characters remain (§5 Q5). + shard account and its characters remain, which is intended (AD8). 6. The demo's four accounts (`demo/mode.js`) must not be deletable. +7. **Nothing stops a deleted name being reused.** Registration and username changes only check + the live `users` table (AD9). ## 4. Design @@ -96,25 +119,27 @@ admin route already refuses. Then a deployment would be left with no one who can by staff and a deletion by the member do the same thing. - **The member's route.** `DELETE /api/v1/auth/me/account`, behind `requireAuth` and `accountChangeLimiter`. - - Body: `{ username, password?, code?, recoveryCode? }`. + - Body: `{ username, password, code?, recoveryCode? }`. - The typed `username` must match the account, ignoring case. - - The password is checked when the account has one. + - The password is always checked (AD7). - A two-factor code or a recovery code is checked when two-factor sign-in is on. - Refusals: - 400 for a username that doesn't match - 401 for a wrong password or code + - 409 `password_required` for an account with no password: the screen points at the + Account page's set-a-password section, which already exists for that case - 409 for the last admin - the demo's own refusal for a demo account (`demo/mode.js`, the same lock other breaking settings use) - `#swagger.*` annotations; the spec is regenerated. - **The procedure, in order:** 1. **Before the delete**, read what is needed afterwards: the email address (for the notice - below) and the linked game ids. + below), the username and the role. 2. **In one transaction:** - - clear the "what they wrote" snapshots (gap 2), so their threads, posts and uploads show - "[deleted account]"; §5 Q2 decides whether the content itself stays - - apply §5 Q3's answer to the "records" snapshots and to their activity-log rows - - delete the `users` row, so the foreign keys do the rest + - when the role is staff (anything but `player`), add the username to a new + `reserved_usernames` table (AD9) + - delete the `users` row, so the foreign keys do the rest. The snapshot columns, the + content and the activity log are **not** touched (AD5, AD6). 3. **After commit**, fire-and-forget, each in its own `try`: - every module's `onUserDeleted` hook (below) - `teams.reconcile({ reason: 'account deleted' })` @@ -126,6 +151,22 @@ admin route already refuses. Then a deployment would be left with no one who can - **Nothing can stop a deletion once it starts.** A module hook that throws is logged and the deletion still stands, because Play requires the request to be honoured. That is also why the hook runs after the commit rather than before it. +- **Who sees the name (AD5, AD6).** A snapshot whose id column is now NULL belongs to a deleted + account; nothing else clears those ids. Rendering follows the viewer: + - **Members**, Team leaders included, and signed-out visitors see `[deleted player]`. That + covers forum threads, posts and uploads, and any record a member can read. The constant in + `teamForum.model.js` and `contentReports.model.js` changes from `[deleted account]`, and the + "id NULL" case now wins over a present snapshot. + - **Staff** (admin, editor, moderator) see the stored username with a *deleted account* + marker, in the Reports queue, the forum's moderation views, the user and activity screens, + and Team moderation requests. The activity log keeps its IP addresses. + - The rule lives in one helper, `displayAuthor(row, viewer)`, which every model that renders a + snapshot uses, and its tests cover each table in gap 2. +- **`reserved_usernames`** (AD9) holds `username_norm` (the primary key, lowercased), `reason` + (`deleted_staff`) and `reserved_at`. Registration, an invite's acceptance, an admin creating an + account, and a username change all refuse a reserved name, with the same message as a taken + one. There is no release path, because the org lead said "completely unavailable". A player's + username is not added and is free at once. - **`MODULE_API` 1.20.0: `api.onUserDeleted(async ({ userId, deletedBy }) => {})`**, where `deletedBy` is `'self' | 'staff'`. - It runs after the row is gone, so a module that keeps data without a foreign key cleans it @@ -139,12 +180,17 @@ admin route already refuses. Then a deployment would be left with no one who can session, trusted device and push registration; linked Google, Discord, OIDC and Steam sign-ins; game-account links and, in Rust, the permissions and groups the site gave those Steam ids; notification settings and the inbox. - - **Stays, without your name:** what you wrote where others read it (§5 Q2), shown as - "[deleted account]"; reports you filed and appeals you made, as moderation records; event - results, with the score kept and the name removed; the delivery log's hashes and the - suppression list (`collection.mjs` already says so). - - **Stays for security:** §5 Q3's answer. -- **Docs:** `BACKEND_DESIGN.md` (route, procedure, the table above), `MODULE_API.md`, OpenAPI. + - **Stays, shown to other members as "[deleted player]":** what you wrote where others read it + (AD5), and reports you filed and appeals you made, as moderation records. + - **Stays, and staff can still see who it was:** the same records with your username, and the + activity log with its IP addresses, for moderation (AD6). Kept until the operator removes + them. + - **Stays, detached:** event results (the score and the in-game name, no site account), the + delivery log's hashes and the suppression list (`collection.mjs` already says so). + - **If you were staff:** your username can never be used again (AD9). +- **Docs:** `BACKEND_DESIGN.md` (the route, the procedure, `reserved_usernames`, the + display rule, the list above), `TEAMS.md` (the forum's deleted-author rule), `MODULE_API.md`, + OpenAPI. ### 4.2 Website client @@ -176,9 +222,9 @@ admin route already refuses. Then a deployment would be left with no one who can - The links and grants themselves already cascade. - Core-ref pin. - **Module-uo:** - - `shard_account_links` already cascades. - - `onUserDeleted` does whatever §5 Q5 decides for the shard account. - - Core-ref pin. + - `shard_account_links` already cascades, and the shard account stays (AD8), so the module + registers no hook. + - Core-ref pin only, if its `coreApi` range needs it. - **Integration Kit:** the re-pin, and a paragraph in the module chapter: what `onUserDeleted` is for, and that a module keeping user data with no foreign key must use it. @@ -187,7 +233,8 @@ admin route already refuses. Then a deployment would be left with no one who can - **`/delete-account/`**, the page Play is given (AD2). It covers: - the app's name and ours - the steps on the website and in the app - - what is deleted and what is kept, and for how long + - what is deleted and what is kept, and for how long: the moderation records that stay, and + that other members see "[deleted player]" - that we hold no account, because each community runs its own site - what to do if you cannot sign in: ask that community's operator, who can delete the account from the admin panel @@ -195,65 +242,25 @@ admin route already refuses. Then a deployment would be left with no one who can `/privacy` and the page agree. `PLAY_DATA_SAFETY.md`'s account-deletion bullet then names the URL and answers "yes" to "can users request deletion". -## 5. Questions for the org lead +## 5. The questions, as they were asked and answered (org lead, 2026-10-29) -- **Q1 — Delete now, or after a grace period?** - - *Example:* a player taps *Delete* at 2 a.m. after a bad raid. - - **Now:** the account is gone, and the username is free to register by morning. - - **Grace period (14 days):** the account is locked at once and signs them out everywhere. - Signing in during the 14 days cancels the deletion, and a nightly sweep deletes it when the - period ends. That needs a `pending_deletion` status and a sweep. - - *Recommended: now.* It is simpler, it is what the confirm step is for, and Play accepts either. -- **Q2 — What happens to what they wrote?** - - *Example:* a member wrote a forum guide with 40 replies under it. - - **Keep it, shown as "[deleted account]":** the thread still reads, with no name on it. - - **Delete their posts and uploads:** the guide is gone, and the replies answer nothing. - - *Recommended: keep it, anonymous.* Most forums work this way, and the screens say so before - the member confirms. -- **Q3 — Records of what was done: the activity log's IP addresses, and the moderation - snapshots.** - - *Example:* a banned player deletes their account and comes back as someone new. Or a Team - leader who hid a post deletes their account, and staff later ask who hid it. - - **Keep them as they are:** moderators can still match the old IPs, and a record still says - who filed the report or hid the post. Those are security and moderation records, and the - privacy notes say so. - - **Clear them:** clear `ip` and the username in `detail` in the activity log, and the - "records" snapshots in gap 2. Nothing can be matched any more, and those records then read - "[deleted account]". - - *Recommended: keep them,* as security and moderation records kept until the operator removes - them, which is what `collection.mjs` already says about the audit log. -- **Q4 — How does an account with no password confirm?** - - *Example:* a player who only ever signs in with Discord, with no two-factor sign-in. - - **Typing the username on a signed-in session is enough:** simple, but a person holding their - unlocked phone can do it. - - **Sign in with the provider again first** (within the last 10 minutes): safer, but it needs a - re-authentication flow on the web and in the app. - - *Recommended: sign in again,* reusing the SSO flow with a `reauth` purpose. -- **Q5 — The UO game account the app can create.** - - *Example:* a player made `Thorne` on the shard from the app and has three characters on it. - - **Leave it on the shard:** the link goes, and the shard account and characters stay. The - shard's staff deal with it as they would any account. - - **Offer to delete it too:** the confirm step adds "Also delete game account Thorne and its - characters". That needs a new bridge command, `account.delete`, and a protocol bump in - servuo-plugins, link and module-uo. - - *Recommended: leave it.* Play's rule concerns the account a user signs in to the app with, - which is the site account, and a shard account belongs to the shard. If Play's review asks, - the second option can follow. -- **Q6 — Can someone else take the username straight away?** - - *Example:* a moderator called `Ravenna` deletes their account, and the next day someone - registers `Ravenna` and messages players as staff. - - **Free at once:** simple. - - **Reserved for 90 days:** a small `reserved_usernames` table that registration and username - changes check. - - *Recommended: reserve for 90 days*, for staff accounts only. +| | Asked, with the example given | Answer | +|---|---|---| +| Q1 | Delete now, or lock for 14 days and let a sign-in cancel it? (a player taps *Delete* at 2 a.m. after a bad raid) | **Delete now** (AD4) | +| Q2 | Keep what they wrote, anonymously, or delete it? (a forum guide with 40 replies) | **Keep it; every username snapshot reads "[deleted player]"** (AD5) | +| Q3 | Keep or clear the activity log's IPs and the moderation snapshots? (a banned player returns under a new name) | **Keep the name and the IP on the admin side, for moderation** (AD6) | +| Q4 | How does an account with no password confirm? (a Discord-only player) | **Every account already has a password; SSO is a convenience on top of a proper account** (AD7) | +| Q5 | What happens to a UO game account the app created? (`Thorne`, with three characters) | **Leave it on the shard** (AD8) | +| Q6 | Can a deleted username be taken at once? (a moderator `Ravenna` impersonated the next day) | **Staff names are completely unavailable** (AD9) | ## 6. Build order -1. **This plan** (docs), with §5's answers. +1. **This plan** (docs), with the answers (AD4-AD9). 2. **Core** (`website`): §4.1 and §4.2, `MODULE_API` 1.20.0, tests (the service's procedure, the - route's refusals, the demo lock, a module hook that throws), OpenAPI, `BACKEND_DESIGN.md`, - `MODULE_API.md`. One PR. -3. **Module-Rust** (`edge`) and **Module-uo**: §4.4 plus their core-ref pins, one PR each. + route's refusals, the demo lock, a module hook that throws, `displayAuthor` per table, a + reserved staff name), OpenAPI, `BACKEND_DESIGN.md`, `TEAMS.md`, `MODULE_API.md`. One PR. +3. **Module-Rust** (`edge`): §4.4's hook plus its core-ref pin. One PR. **Module-uo** gets a + pin only if its `coreApi` range needs one. **Integration Kit**: the re-pin and the paragraph, one PR. 4. **Android** (`edge`): §4.3. One PR. 5. **runicgateway.com**: §4.5, plus `PLAY_DATA_SAFETY.md` regenerated. One PR. @@ -261,7 +268,9 @@ admin route already refuses. Then a deployment would be left with no one who can - delete on the website, and in the app on the emulator - the other device's session dies on its next request - a Rust-linked account's grants leave the rig on the next sync - - a forum post shows "[deleted account]" + - a forum post shows "[deleted player]" to a member and the old username, marked deleted, to + staff + - the deleted staff username is refused at registration and at a username change - the demo refuses - the last admin is refused 7. Then `USER_SAFETY.md` (report, block and hide, now 1.21.0), then `android/PLAN.md` M8's -- 2.49.1 From fc3f0ed29690c79acf10704ec9545d0370d46eb4 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 29 Oct 2026 17:28:54 -0500 Subject: [PATCH 5/5] docs(website): deletion unlinks game accounts through each module's unlink A cascade is not an unlink. For UO the link lives on the shard (the site's shard_account_links is a mirror), so dropping the mirror alone would leave the shard attributing the game account to a deleted user. - AD10: the hook is onUserDeleting and runs before the delete; each module calls its own unlink for every link (Rust: links.unlinkOwned; UO: the sidecar's DELETE /link/{account}, as the staff unlink does). - AD11: an unlink that can't finish never holds up the deletion. A shard that is offline queues into shard_pending_unlinks, retried on reconnect. A protected staff account (403) is listed for admins as "still linked on the shard". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- README.md | 2 +- website/ACCOUNT_DELETION.md | 100 +++++++++++++++++++++++++++--------- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 90843ed..74a70d1 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ sidecar as a service, and hands you the values the website needs. | [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework | | [TRUSTED_DEVICES_MFA.md](website/TRUSTED_DEVICES_MFA.md) | TOTP two-factor, trusted devices and recovery codes | | [STEAM_SIGNIN.md](website/STEAM_SIGNIN.md) | **Plan, built 2026-10-19** — Sign in with Steam as a built-in provider beside Google and Discord (OpenID 2.0), and a Steam id for modules | -| [ACCOUNT_DELETION.md](website/ACCOUNT_DELETION.md) | **Plan, 2026-10-29**: a member deletes their own account on the website and in the app, as Google Play requires. One deletion service for staff and self, an `onUserDeleted` module hook (`MODULE_API` 1.20.0), and runicgateway.com's `/delete-account/` page as the address Play is given | +| [ACCOUNT_DELETION.md](website/ACCOUNT_DELETION.md) | **Plan, 2026-10-29**: a member deletes their own account on the website and in the app, as Google Play requires. One deletion service for staff and self, an `onUserDeleting` module hook that unlinks game accounts through each module's own unlink (`MODULE_API` 1.20.0), and runicgateway.com's `/delete-account/` page as the address Play is given | | [USER_SAFETY.md](website/USER_SAFETY.md) | **Plan, 2026-10-29** — report, block and hide on the website and in the app: an account-wide block, a member's own hide and staff's hide for everyone, with module-owned report targets (`MODULE_API` 1.21.0, after account deletion) | | [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, fully decided 2026-10-23** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | | [MODERATION_APPEALS.md](website/MODERATION_APPEALS.md) | Moderation actions, content reports and the appeals flow | diff --git a/website/ACCOUNT_DELETION.md b/website/ACCOUNT_DELETION.md index fef2808..9ea1263 100644 --- a/website/ACCOUNT_DELETION.md +++ b/website/ACCOUNT_DELETION.md @@ -1,6 +1,6 @@ # Account deletion — a member deletes their own account, on the website and in the app -**Status:** plan of record, 2026-10-29, with every question answered (AD4-AD9). Nothing built +**Status:** plan of record, 2026-10-29, with every question answered (AD4-AD11). Nothing built yet. Each phase in §6 stops for the org lead before the next one starts. **Why now.** Google Play's Data safety form asks whether the app lets people create an account, and @@ -58,6 +58,21 @@ The org lead's answers to the six questions in §5, the same day: - **AD9 — A deleted staff account's username is never available again** (Q6). It can never be registered, or taken by a username change. A player's username is free at once. +Asked after the plan was first written ("will deletion call the unlink function for that +player?"), the same day: + +- **AD10 — Deletion unlinks through each module's own unlink, before the row goes.** A database + cascade is not an unlink. For UO, the link lives on the shard (gap 8), so dropping the site's + mirror would leave the shard attributing the game account to a user who no longer exists. The + hook is therefore `onUserDeleting` and runs before the delete. Each module calls the unlink it + already has, for every link the person holds, so the game sees exactly what a player's own + unlink produces. +- **AD11 — An unlink that cannot finish never holds up the deletion.** + - A shard that is offline (503) gets a pending unlink in the module, retried when the bridge + reconnects. + - A **protected staff account**, which the shard refuses to unlink (403), is logged and shown to + admins as *still linked on the shard*, for the shard's staff to clear by hand. + ## 3. What exists today (read 2026-10-29, website `main` 222bcb0) - **The admin route.** `DELETE /api/v1/admin/users/:id` refuses your own account and the last admin, @@ -75,7 +90,7 @@ The org lead's answers to the six questions in §5, the same day: `rust_permgroup_members`, all **CASCADE**. Module-Rust's grants and group memberships keyed straight to a Steam id (`rust_perm_steam_grants`, `rust_permgroup_steam_members`) have no site account behind them, and stay. Staff gave them to a player in the game, not to a member - of the site. + of the site. **A cascade is not an unlink, though** (gap 8, AD10). - **Sessions die at once.** `requireAuth` re-reads the user on every request and answers 401 when the row is gone (`session.middleware.js`). So a deleted member's web cookie or mobile access token stops working on its next use, without waiting for it to expire. @@ -109,6 +124,17 @@ The org lead's answers to the six questions in §5, the same day: 6. The demo's four accounts (`demo/mode.js`) must not be deletable. 7. **Nothing stops a deleted name being reused.** Registration and username changes only check the live `users` table (AD9). +8. **A cascade skips the modules' unlink, and for UO that leaves the shard linked.** + - **UO:** `shard_account_links` is only a mirror. The shard keeps the site user id on the game + account (`INTEGRATION.md`: "permanently tagged with your `websiteUserId`"), and every + `mob.login` reports it as `webId`. That is why the staff unlink in `usersShard.controller.js` + calls the sidecar's `DELETE /link/{account}` first and drops the mirror after. A cascade + alone would leave the shard, and the sidecar's own link mirror, attributing the account to a + deleted user forever, with every login still carrying that `webId`. + - **Rust:** the site's `rust_account_links` is the only record; the plugin keeps no link state. + The cascade gets the same result as `links.unlinkOwned`, apart from its + `rust.account.unlink` log entry and its prompt Teams re-sync. Calling the real function keeps + both. ## 4. Design @@ -134,23 +160,24 @@ The org lead's answers to the six questions in §5, the same day: - `#swagger.*` annotations; the spec is regenerated. - **The procedure, in order:** 1. **Before the delete**, read what is needed afterwards: the email address (for the notice - below), the username and the role. + below), the username and the role. Then run every module's `onUserDeleting` hook (below), in + turn, each in its own `try`. That is where game links are unlinked (AD10). 2. **In one transaction:** - when the role is staff (anything but `player`), add the username to a new `reserved_usernames` table (AD9) - delete the `users` row, so the foreign keys do the rest. The snapshot columns, the content and the activity log are **not** touched (AD5, AD6). 3. **After commit**, fire-and-forget, each in its own `try`: - - every module's `onUserDeleted` hook (below) - `teams.reconcile({ reason: 'account deleted' })` - an activity-log entry, `user.delete` (by staff) or `user.self_delete`, naming the id and never the username - an email to the address read in step 1, when there was one and mail is configured: "your account on was deleted on ". This is the only notice a person gets if someone else used their unlocked phone, and it says who to contact. - - **Nothing can stop a deletion once it starts.** A module hook that throws is logged and the - deletion still stands, because Play requires the request to be honoured. That is also why the - hook runs after the commit rather than before it. + - **Nothing can stop a deletion once it starts.** A module hook that throws, or that takes too + long, is logged, and the deletion goes ahead, because Play requires the request to be + honoured. Core gives each hook a time limit (10 seconds) so a hung game server cannot hold a + member's request open; whatever the module had not finished is its pending work (AD11). - **Who sees the name (AD5, AD6).** A snapshot whose id column is now NULL belongs to a deleted account; nothing else clears those ids. Rendering follows the viewer: - **Members**, Team leaders included, and signed-out visitors see `[deleted player]`. That @@ -167,11 +194,16 @@ The org lead's answers to the six questions in §5, the same day: account, and a username change all refuse a reserved name, with the same message as a taken one. There is no release path, because the org lead said "completely unavailable". A player's username is not added and is free at once. -- **`MODULE_API` 1.20.0: `api.onUserDeleted(async ({ userId, deletedBy }) => {})`**, where +- **`MODULE_API` 1.20.0: `api.onUserDeleting(async ({ userId, deletedBy }) => {})`**, where `deletedBy` is `'self' | 'staff'`. - - It runs after the row is gone, so a module that keeps data without a foreign key cleans it - up, and a module with game-side effects acts on them at once. It must not throw. - - It is the same staging and validation as `onIdentityLinked`. + - It runs **before** the row goes, while the module can still read the person's links (AD10). + Its job is to undo in the game, and anywhere else the module holds state, what the site's + rows are about to stop saying: unlink each game link through the module's own unlink, and + clean up data kept without a foreign key. + - It cannot veto. Throwing, or running past core's time limit, is logged, and the deletion + goes ahead (AD11). A module that cannot finish keeps its own record of what is left, and + retries. + - It is the same staging and validation as `onIdentityLinking`. - `MODULE_API.md` §2.4 and §2.5 are amended. The Integration Kit's `coreApi` check goes red until it is re-pinned, which is how the kit is meant to notice. - **What goes and what stays** is written once in `BACKEND_DESIGN.md`, and the screens and the @@ -217,16 +249,34 @@ The org lead's answers to the six questions in §5, the same day: ### 4.4 Modules - **Module-Rust (`edge`):** - - `onUserDeleted` marks the permission sync dirty, so withdrawn grants reach the game on the - next tick rather than waiting for the digest check. - - The links and grants themselves already cascade. + - `onUserDeleting` calls `links.unlinkOwned(steamId, userId)` for each Steam id the person + holds, the same call the player's own unlink makes. That keeps its `rust.account.unlink` + entry (with `detail.reason: 'account deleted'`) and its Teams re-sync. + - It then marks the permission sync dirty, so withdrawn grants reach the game on the next tick + rather than waiting for the digest check. + - It never fails on the game: the link is the site's own record and the plugin keeps none, so + there is nothing pending. - Core-ref pin. - **Module-uo:** - - `shard_account_links` already cascades, and the shard account stays (AD8), so the module - registers no hook. - - Core-ref pin only, if its `coreApi` range needs it. -- **Integration Kit:** the re-pin, and a paragraph in the module chapter: what `onUserDeleted` is - for, and that a module keeping user data with no foreign key must use it. + - `onUserDeleting` calls the staff unlink's path for each game account in + `shard_account_links`: `uoLinkClient.unlinkAccount({ actor, account })`, where `actor` is + `web:account-deletion`, then `shardLinks.removeByAccount(account)` on success. That clears + the shard's `WebsiteUserId` tag, so attribution stops at once. The shard account and its + characters stay (AD8). + - **Shard offline** (503, or no sidecar configured): the account goes into a new + `shard_pending_unlinks` table (`account`, `queued_at`, `attempts`, `last_error`), and the + mirror row goes with the cascade. The module retries the queue at boot and whenever the + bridge reconnects, until the shard answers 200 or 404 (404 means the shard has nothing to + clear). + - **Protected staff account** (403): the shard refuses by design, so retrying will not help. + The account goes into the same table marked `refused`. Admin → Shard shows it as *still + linked on the shard* until shard staff clear it by hand, and the module drops the row once + the shard stops reporting that `webId`. + - Core-ref pin. +- **Integration Kit:** the re-pin, and a paragraph in the module chapter. `onUserDeleting` is + where a module unlinks anything a game holds for the person, through its own unlink, and cleans + up data kept with no foreign key. It can't stop the deletion, so a module that talks to a game + needs a retry for when the game is away. ### 4.5 runicgateway.com @@ -259,15 +309,19 @@ The org lead's answers to the six questions in §5, the same day: 2. **Core** (`website`): §4.1 and §4.2, `MODULE_API` 1.20.0, tests (the service's procedure, the route's refusals, the demo lock, a module hook that throws, `displayAuthor` per table, a reserved staff name), OpenAPI, `BACKEND_DESIGN.md`, `TEAMS.md`, `MODULE_API.md`. One PR. -3. **Module-Rust** (`edge`): §4.4's hook plus its core-ref pin. One PR. **Module-uo** gets a - pin only if its `coreApi` range needs one. - **Integration Kit**: the re-pin and the paragraph, one PR. +3. **Module-Rust** (`edge`) and **Module-uo**: §4.4's hooks plus their core-ref pins, one PR + each. Module-uo's PR adds `shard_pending_unlinks`, the retry, and the *still linked on the + shard* list. **Integration Kit**: the re-pin and the paragraph, one PR. 4. **Android** (`edge`): §4.3. One PR. 5. **runicgateway.com**: §4.5, plus `PLAY_DATA_SAFETY.md` regenerated. One PR. 6. **A walk**, using a throwaway account on each surface: - delete on the website, and in the app on the emulator - the other device's session dies on its next request - - a Rust-linked account's grants leave the rig on the next sync + - a Rust-linked account's grants leave the rig on the next sync, with a + `rust.account.unlink` entry + - a UO-linked account (the local ServUO with the uo-link sidecar): the shard's link is cleared, + and the next login carries no `webId`. With the sidecar stopped, the unlink waits in + `shard_pending_unlinks` and clears when the sidecar comes back - a forum post shows "[deleted player]" to a member and the old username, marked deleted, to staff - the deleted staff username is refused at registration and at a username change -- 2.49.1