feat(federation): S2S surfaces exclude detached accounts — tier-2, profile_update, identity delete (detach spec §4.3)

This commit is contained in:
Jannis Braun
2026-07-02 18:39:54 +02:00
parent d37919b5a0
commit 68be2e26b1
4 changed files with 248 additions and 1 deletions
+23
View File
@@ -2322,6 +2322,15 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
return reply.code(403).send({ error: 'Attribution mismatch: you can only delete users from your own instance', statusCode: 403 });
}
// Detached (home-orphaned) accounts are sovereign local accounts. The
// domain's new incarnation must not delete them by replaying old
// homeUserIds. Idempotent 200: from the caller's perspective this
// identity does not exist here.
if (user.federationHomeOrphaned === 1) {
console.log(`[federation] Ignoring S2S identity delete for detached account ${user.id} from ${fedHeaders.origin}`);
return reply.code(200).send({ success: true });
}
// 5. Check for owned spaces
const ownedSpaces = db.select({ id: schema.spaces.id, name: schema.spaces.name })
.from(schema.spaces)
@@ -3478,6 +3487,10 @@ export function findFederatedUser(
and(
eq(schema.users.homeInstance, domain),
eq(schema.users.isDeleted, 0),
// Detached (home-orphaned) accounts are sovereign: never re-bindable to
// the domain's new incarnation via username heuristics — that is exactly
// how a new same-name user would capture the established account.
eq(schema.users.federationHomeOrphaned, 0),
or(
sql`lower(substr(${schema.users.username}, 1, instr(${schema.users.username}, '@') - 1)) = ${hintLower}`,
and(
@@ -6110,6 +6123,16 @@ export async function processProfileUpdateEvent(
return;
}
// Detached accounts are sovereign: the domain now belongs to a different
// incarnation, which must never overwrite the established account's profile
// by replaying its old homeUserId. Ack (not reject) — the sender considers
// this identity theirs to update; from our side the update simply no-ops.
if (localUser.federationHomeOrphaned === 1) {
console.log(`[federation] Skipping profile_update for detached account ${localUser.id} (home-orphaned)`);
accepted.push(event.messageId);
return;
}
// Version check: reject stale/duplicate events
const storedTs = localUser.profileUpdatedAt ?? 0;
const incomingTs = payload.profileUpdatedAt ?? 0;