From 582a55dde5041b7ef02dab019004eaca36bab165 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 29 Oct 2026 20:38:33 -0500 Subject: [PATCH] feat(account): unlink game accounts on the shard before an account is deleted (MODULE_API 1.20.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ACCOUNT_DELETION.md §4.4 (AD10, AD11). api.onUserDeleting queues every linked account in the new shard_pending_unlinks table, then calls the sidecar's DELETE /link/{account} (actor web:account-deletion) and drops the mirror on 200/404. A shard that is away leaves the row pending, retried on WS reconnect and on a live server.hello; a protected account (403) is refused and listed on Admin -> Shard as still linked on the shard. A live mob.login without the deleted user's webId clears the row. coreApi ^1.20.0; core-ref pinned to website#232's merge (7bd93b8). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- ci/core-ref.json | 6 +- client/src/routes/admin/ShardAdmin.jsx | 35 ++ module.json | 2 +- server/db/purge.sql | 4 + server/db/schema.sql | 24 ++ server/index.js | 8 + .../shardPendingUnlinks.db.js | 50 +++ server/router/admin/uoLink.controller.js | 7 +- server/router/admin/uoLink.router.js | 3 +- server/test/_fakes.js | 2 + server/test/accountDeletion.test.js | 312 ++++++++++++++++++ server/test/entry.test.js | 2 + server/utils/accountDeletion.js | 154 +++++++++ server/utils/shardIngest.js | 21 +- server/utils/uoLinkSocket.js | 5 + swagger-fragment.json | 4 +- 16 files changed, 630 insertions(+), 9 deletions(-) create mode 100644 server/model/shardPendingUnlinks/shardPendingUnlinks.db.js create mode 100644 server/test/accountDeletion.test.js create mode 100644 server/utils/accountDeletion.js diff --git a/ci/core-ref.json b/ci/core-ref.json index 2afa537..2d7d07c 100644 --- a/ci/core-ref.json +++ b/ci/core-ref.json @@ -1,6 +1,6 @@ { - "$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/uo and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves. Pinned rather than tracking a branch on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together. **It pointed at `edge` for the length of the Event System window** (org lead, 2026-09-04), and this commit ends that: `api.registerEventActions` exists only from MODULE_API 1.10.0, so under the previous `main` pin `register()` threw and the module did not load at all — the job would have been red by construction for eight phases and would have proved nothing while a real regression hid behind it. The Phase 16b cutover put 1.10.0 on `main`, so the pin comes home, and this is the same move that turns the Integration kit green again. **routes.manifest.json needed NO regeneration**: the job's own steps were run against this exact ref and answered `routes.manifest.json is current — 73 routes, all documented`, so the \"commit both together\" instruction above had nothing to pair with this time.", + "$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/uo and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves. Pinned rather than tracking a branch on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together. **It pointed at `edge` for the length of the Event System window** (org lead, 2026-09-04), and this commit ends that: `api.registerEventActions` exists only from MODULE_API 1.10.0, so under the previous `main` pin `register()` threw and the module did not load at all — the job would have been red by construction for eight phases and would have proved nothing while a real regression hid behind it. The Phase 16b cutover put 1.10.0 on `main`, so the pin comes home, and this is the same move that turns the Integration kit green again. **routes.manifest.json needed NO regeneration**: the job's own steps were run against this exact ref and answered `routes.manifest.json is current — 73 routes, all documented`, so the \"commit both together\" instruction above had nothing to pair with this time. **2026-10-30, account deletion (ACCOUNT_DELETION.md §4.4):** this module registers onUserDeleting (MODULE_API 1.20.0), so coreApi is ^1.20.0 and the pin moves past 1.11.0-1.19.0 to `main` at the sha website#232 merged as. A 1.19.0 core has no api.onUserDeleting, so register() would throw under the old pin.", "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", - "ref": "655fbf3f69a6a1fd650ecbc81afd6cf9c2ad9f66", - "refName": "main @ MODULE_API 1.10.0, the Event System cutover (website#199)" + "ref": "7bd93b8bb0da734a642587f256d10c523ca39bea", + "refName": "main @ MODULE_API 1.20.0, account deletion (website#232)" } diff --git a/client/src/routes/admin/ShardAdmin.jsx b/client/src/routes/admin/ShardAdmin.jsx index f1e168c..6d0fe01 100644 --- a/client/src/routes/admin/ShardAdmin.jsx +++ b/client/src/routes/admin/ShardAdmin.jsx @@ -83,6 +83,39 @@ function StatusPanel({ config }) { ) } +// ── Unlinks a deleted website account left behind ─────────────────────────── +// +// ACCOUNT_DELETION.md §4.4. Deleting a website account unlinks its game accounts +// on the shard first; what the shard could not take is listed here until it +// clears. `pending` is retried by itself when the bridge reconnects. `refused` is +// a protected staff account the shard will not unlink over the bridge, so the +// shard's staff clear its WebsiteUserId tag by hand. A row goes on its own once a +// login on that account no longer carries the deleted user's id. +function PendingUnlinks({ rows }) { + if (!rows || rows.length === 0) return null + return ( +
+

Still linked on the shard

+

+ Game accounts whose website account was deleted, but whose link the shard has not cleared yet. +

+
    + {rows.map((r) => ( +
  • + {r.account} + · deleted user #{r.webUserId} · {new Date(r.queuedAt).toLocaleString()} +
    + {r.state === 'refused' + ? 'Protected staff account: the shard will not unlink it over the bridge. Clear its link on the shard by hand.' + : `Waiting for the shard; tried ${r.attempts} time${r.attempts === 1 ? '' : 's'}, and tried again when the bridge reconnects.`} +
    +
  • + ))} +
+
+ ) +} + // ── Game-account signup ───────────────────────────────────────────────────── // // This field lived in core's Site Settings until slice 3 of the extraction. It @@ -285,6 +318,8 @@ export default function ShardAdmin() { + +