feat(account): unlink game accounts on the shard before an account is deleted (MODULE_API 1.20.0)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 1m0s
PR Checks / client-build (pull_request) Successful in 1m0s
PR Checks / frozen-manifest (pull_request) Successful in 1m20s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-29 20:38:33 -05:00
parent 6246383962
commit 582a55dde5
16 changed files with 630 additions and 9 deletions

View File

@@ -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 (
<section style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 16, display: 'flex', flexDirection: 'column', gap: 10 }}>
<h3 className="display" style={{ margin: 0, fontSize: '1.05rem', color: 'var(--head)' }}>Still linked on the shard</h3>
<p className="sans dim" style={{ margin: 0, fontSize: '0.82rem' }}>
Game accounts whose website account was deleted, but whose link the shard has not cleared yet.
</p>
<ul style={{ margin: 0, padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 8 }}>
{rows.map((r) => (
<li key={r.account} className="sans" style={{ fontSize: '0.85rem', color: 'var(--ink)' }}>
<strong>{r.account}</strong>
<span className="dim"> · deleted user #{r.webUserId} · {new Date(r.queuedAt).toLocaleString()}</span>
<div className="dim" style={{ fontSize: '0.78rem', marginTop: 2 }}>
{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.`}
</div>
</li>
))}
</ul>
</section>
)
}
// ── 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() {
<StatusPanel config={config} />
<PendingUnlinks rows={config.pendingUnlinks} />
<Toggle checked={enabled} onChange={setEnabled} label="Enable the shard integration" />
<label style={{ display: 'block' }}>