feat(account): unlink game accounts on the shard before an account is deleted (MODULE_API 1.20.0)
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:
@@ -21,6 +21,7 @@ const { settings: settingsModel, events: coreEvents } = require('../core')
|
||||
const broadcaster = require('./shardBroadcast')
|
||||
const shardPush = require('./shardPush')
|
||||
const shardEngagement = require('./shardEngagement')
|
||||
const accountDeletion = require('./accountDeletion')
|
||||
const defaultLog = require('../core').logger('shard-ingest')
|
||||
|
||||
// Notable kinds appended to the shard_events log. High-frequency/session kinds
|
||||
@@ -103,7 +104,7 @@ async function resolveShardName(shard, deps) {
|
||||
|
||||
// Apply the state-change side effect for a kind (if any). Returns a promise.
|
||||
async function applyStateChange(event, deps) {
|
||||
const { shardState, uoLinkConfig, eventsReconcile, fromBackfill, log } = deps
|
||||
const { shardState, uoLinkConfig, eventsReconcile, pendingUnlinks, fromBackfill, log } = deps
|
||||
switch (event.kind) {
|
||||
case 'server.hello': {
|
||||
const incoming = event.bootId || null
|
||||
@@ -142,6 +143,10 @@ async function applyStateChange(event, deps) {
|
||||
// missed by skipping these — core asks every module at its own boot.
|
||||
eventsReconcile()
|
||||
}
|
||||
// The shard is (back) on the bridge, so an unlink an account deletion left
|
||||
// queued can be asked again. Live only, like the reconcile above: a replay
|
||||
// says nothing about the shard being up now. Not awaited; it never rejects.
|
||||
if (!fromBackfill) pendingUnlinks.retryPending()
|
||||
return
|
||||
}
|
||||
case 'server.shutdown':
|
||||
@@ -162,6 +167,16 @@ async function applyStateChange(event, deps) {
|
||||
y: event.y,
|
||||
z: event.z,
|
||||
})
|
||||
// A queued unlink whose account now logs in without the deleted user's id
|
||||
// is done. Live only: a replayed login from before the link carries no
|
||||
// `webId` and would clear a row whose tag is still on the shard.
|
||||
if (!fromBackfill && who.acct) {
|
||||
try {
|
||||
await pendingUnlinks.seenLogin({ account: who.acct, webId: event.webId })
|
||||
} catch (err) {
|
||||
log.warn('could not check a login against the queued unlinks', { message: err.message })
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'mob.logout': {
|
||||
@@ -323,6 +338,10 @@ function resolveDeps(deps) {
|
||||
// reconcile must be able to see the call without a live event engine behind
|
||||
// it.
|
||||
eventsReconcile: deps.eventsReconcile || (() => coreEvents.reconcile()),
|
||||
// MODULE_API 1.20.0 (ACCOUNT_DELETION.md §4.4): the unlinks an account
|
||||
// deletion could not finish, retried when the shard says hello and cleared
|
||||
// when a login shows the tag gone.
|
||||
pendingUnlinks: deps.pendingUnlinks || accountDeletion,
|
||||
// Not injectable — it is the caller's statement about this frame rather than
|
||||
// a dependency. It reaches `applyStateChange` because the reconcile below is
|
||||
// the one state change that must not act on a replay; see the note there.
|
||||
|
||||
Reference in New Issue
Block a user