fix(federation): close detached-account gaps from final review — presence/hydrate guards, ack re-detect clear, self-delete password (detach spec §4.3/§4.4/§4.6)

This commit is contained in:
Jannis Braun
2026-07-02 19:34:32 +02:00
parent 172398171a
commit 13d050c1bb
10 changed files with 190 additions and 8 deletions
@@ -149,6 +149,47 @@ describe('markPeerReset — detection-only reset routing', () => {
expect(row.resolvedAt).toBeNull();
});
it('re-detected reset clears a stale acknowledgedAt (dismissed card re-surfaces)', async () => {
seedPeer();
seedUser('stub-1', { passwordHash: STUB });
seedUser('real-1', { passwordHash: '$2b$10$realbcrypthash' });
const { markPeerReset } = await import('./federationReset.js');
// First reset detected, then the admin dismisses (acknowledges) the card.
markPeerReset('peer-1', ORIGIN, 'E0', 'E1');
testDb.update(schema.federationResetEvents)
.set({ acknowledgedAt: Date.now() })
.where(eq(schema.federationResetEvents.origin, ORIGIN)).run();
expect(testDb.select().from(schema.federationResetEvents)
.where(eq(schema.federationResetEvents.origin, ORIGIN)).get()!.acknowledgedAt).not.toBeNull();
// The peer resets AGAIN before the first was resolved — a fresh batch is
// detached and needs fresh admin attention, so the dismissal must clear.
markPeerReset('peer-1', ORIGIN, 'E0', 'E2');
expect(testDb.select().from(schema.federationResetEvents)
.where(eq(schema.federationResetEvents.origin, ORIGIN)).get()!.acknowledgedAt).toBeNull();
});
it('a resolved+acknowledged prior reset is re-armed (acknowledgedAt cleared) on a new reset', async () => {
seedPeer();
seedUser('stub-1', { passwordHash: STUB });
const { markPeerReset } = await import('./federationReset.js');
markPeerReset('peer-1', ORIGIN, 'E0', 'E1');
// Simulate the heal resolving the first reset AND the admin dismissing it.
testDb.update(schema.federationResetEvents)
.set({ resolvedAt: Date.now(), newEpoch: 'E1', acknowledgedAt: Date.now() })
.where(eq(schema.federationResetEvents.origin, ORIGIN)).run();
// Brand-new reset lands (fresh-journal / onConflictDoUpdate branch).
markPeerReset('peer-1', ORIGIN, 'E1', 'E2');
const row = testDb.select().from(schema.federationResetEvents)
.where(eq(schema.federationResetEvents.origin, ORIGIN)).get()!;
expect(row.resolvedAt).toBeNull();
expect(row.acknowledgedAt).toBeNull();
});
it('matches home_instance stored as a full URL (defensive format match)', async () => {
seedPeer();
// Legacy straggler stored with the https:// prefix rather than bare domain.
+9 -2
View File
@@ -113,9 +113,12 @@ export function markPeerReset(peerId: string, origin: string, deadEpoch: string,
if (existing && existing.resolvedAt === null) {
// Double-reset: keep the ORIGINAL dead_epoch + detected_at (the
// incarnation already snapshotted), refresh counts only. Never overwrite
// dead_epoch on an unresolved row.
// dead_epoch on an unresolved row. Clear `acknowledged_at`: a re-detected
// reset is a fresh event that detached a new batch and needs fresh admin
// attention — a stale dismissal must not keep the disposition card hidden
// (detach spec §4.6).
tx.update(schema.federationResetEvents)
.set({ stubCount, orphanedAccountCount })
.set({ stubCount, orphanedAccountCount, acknowledgedAt: null })
.where(eq(schema.federationResetEvents.origin, origin))
.run();
} else {
@@ -140,6 +143,10 @@ export function markPeerReset(peerId: string, origin: string, deadEpoch: string,
resolvedAt: null,
stubCount,
orphanedAccountCount,
// Re-arm the admin surface: a fresh reset on a previously
// resolved+dismissed origin must clear the old dismissal so the
// new detached batch's disposition card re-surfaces (detach §4.6).
acknowledgedAt: null,
},
})
.run();