fix(migration): require at least one stub in merge pair to prevent false positives

Two real accounts from the same remote instance sharing a 1-on-1 DM
are different people, not duplicates. The shared DM is a legitimate
relay. Only merge when at least one user has passwordHash =
'!federation-replicated' (a relay-created stub).
This commit is contained in:
Jannis Braun
2026-03-31 05:08:22 +02:00
parent c7bdfef038
commit 37fc52d85b
+12 -1
View File
@@ -698,8 +698,12 @@ export function runMigrations(db: Database.Database): void {
// different homeUserId (e.g., auth registration vs S2S relay). Now that // different homeUserId (e.g., auth registration vs S2S relay). Now that
// homeInstance is normalized to bare domain, we can detect and merge duplicates. // homeInstance is normalized to bare domain, we can detect and merge duplicates.
// //
// Safety guard: at least one user in the pair must be a stub
// (passwordHash = '!federation-replicated'). Two real accounts from the same
// instance are different people who share a relayed DM, not duplicates.
//
// Detection criteria (at least one must match, PLUS same homeInstance domain): // Detection criteria (at least one must match, PLUS same homeInstance domain):
// 1. Shared 1-on-1 DM membership (strongest signal) // 1. Shared 1-on-1 DM membership (only when at least one is a stub)
// 2. Username cross-reference (one's homeUserId in the other's username base) // 2. Username cross-reference (one's homeUserId in the other's username base)
// 3. homeUserId cross-match (same homeUserId, missed due to old format mismatch) // 3. homeUserId cross-match (same homeUserId, missed due to old format mismatch)
// //
@@ -738,6 +742,13 @@ export function runMigrations(db: Database.Database): void {
for (let j = i + 1; j < users.length; j++) { for (let j = i + 1; j < users.length; j++) {
const a = users[i]!; const a = users[i]!;
const b = users[j]!; const b = users[j]!;
// Safety guard: at least one must be a stub. Two real accounts from the
// same instance are different people (they share relayed DMs, not identities).
const aIsStub = a.password_hash === '!federation-replicated';
const bIsStub = b.password_hash === '!federation-replicated';
if (!aIsStub && !bIsStub) continue;
let reason: string | null = null; let reason: string | null = null;
// Criterion 1: shared 1-on-1 DM membership // Criterion 1: shared 1-on-1 DM membership