Improve handle registry healing and gossip propagation

Added healing utilities to update the handle registry when a user profile is found at a new node domain. Enhanced upsertHandleEntries to accept a sourceDomain parameter, allowing authoritative updates from the owning node. Updated gossip logic to pass sender/target domains for proper trust handling during handle propagation. Integrated healing logic into chat key lookup to keep the registry accurate.
This commit is contained in:
Christopher
2026-01-28 06:55:13 -08:00
parent 213dcc8095
commit 36f653d500
4 changed files with 111 additions and 18 deletions
+4
View File
@@ -3,6 +3,7 @@ import { db } from '@/db';
import { chatDeviceBundles, handleRegistry, remoteIdentityCache } from '@/db/schema'; import { chatDeviceBundles, handleRegistry, remoteIdentityCache } from '@/db/schema';
import { requireAuth } from '@/lib/auth'; import { requireAuth } from '@/lib/auth';
import { eq, and, gt } from 'drizzle-orm'; import { eq, and, gt } from 'drizzle-orm';
import { updateRegistryFromProfile } from '@/lib/swarm/healing';
/** /**
* GET /api/chat/keys?did=<did> * GET /api/chat/keys?did=<did>
@@ -115,6 +116,9 @@ export async function GET(request: NextRequest) {
if (chatKey) { if (chatKey) {
console.log('[Chat Keys GET] Found key in Swarm Profile'); console.log('[Chat Keys GET] Found key in Swarm Profile');
// HEALING: Update the registry because we found the user at this nodeDomain!
await updateRegistryFromProfile(handleEntry.handle, did, nodeDomain);
// Cache it // Cache it
await db.insert(remoteIdentityCache).values({ await db.insert(remoteIdentityCache).values({
did: did, did: did,
+24 -2
View File
@@ -11,7 +11,11 @@ export type HandleEntry = {
export const normalizeHandle = (handle: string) => export const normalizeHandle = (handle: string) =>
handle.toLowerCase().replace(/^@/, '').trim(); handle.toLowerCase().replace(/^@/, '').trim();
export async function upsertHandleEntries(entries: HandleEntry[]) { // [Modified] Added sourceDomain parameter
export async function upsertHandleEntries(
entries: HandleEntry[],
sourceDomain?: string
) {
if (!db) { if (!db) {
return { added: 0, updated: 0 }; return { added: 0, updated: 0 };
} }
@@ -29,6 +33,9 @@ export async function upsertHandleEntries(entries: HandleEntry[]) {
where: eq(handleRegistry.handle, cleanHandle), where: eq(handleRegistry.handle, cleanHandle),
}); });
// If no timestamp provided, treat it as "now" but be careful
// Actually, if it's missing, it might be old data.
// But if it comes from the authoritative source, we might trust it.
const incomingUpdatedAt = entry.updatedAt ? new Date(entry.updatedAt) : new Date(); const incomingUpdatedAt = entry.updatedAt ? new Date(entry.updatedAt) : new Date();
if (!existing) { if (!existing) {
@@ -42,7 +49,22 @@ export async function upsertHandleEntries(entries: HandleEntry[]) {
continue; continue;
} }
if (!existing.updatedAt || incomingUpdatedAt > existing.updatedAt) { // PROPAGATION FIX:
// 1. If the update comes from the node that OWNS the handle (sourceDomain == entry.nodeDomain),
// we treat it as authoritative.
// 2. We allow updates if the timestamp is newer OR equal (to handle clock skew).
// 3. We allow updates if the authoritative source is correcting a mismatch (e.g. we thought it was distinct, they say it's them).
const isAuthoritative = sourceDomain && (sourceDomain === entry.nodeDomain);
const isNewerOrEqual = incomingUpdatedAt.getTime() >= (existing.updatedAt?.getTime() || 0);
// If authoritative, we accept it even if timestamps are identical (recovery)
// If not authoritative, only accept strictly newer
const shouldUpdate = isAuthoritative
? isNewerOrEqual || (existing.nodeDomain !== entry.nodeDomain) // Auto-correct wrong domain
: incomingUpdatedAt > (existing.updatedAt || new Date(0));
if (shouldUpdate) {
await db.update(handleRegistry) await db.update(handleRegistry)
.set({ .set({
did: entry.did, did: entry.did,
+4 -2
View File
@@ -91,7 +91,8 @@ export async function processGossip(
// Process incoming handles // Process incoming handles
let handlesResult = { added: 0, updated: 0 }; let handlesResult = { added: 0, updated: 0 };
if (payload.handles && payload.handles.length > 0) { if (payload.handles && payload.handles.length > 0) {
handlesResult = await upsertHandleEntries(payload.handles); // PASS SENDER: This allows us to trust "authoritative" updates from the node itself
handlesResult = await upsertHandleEntries(payload.handles, payload.sender);
} }
// Build our response with nodes/handles to share back // Build our response with nodes/handles to share back
@@ -163,7 +164,8 @@ export async function gossipToNode(
let handlesResult = { added: 0, updated: 0 }; let handlesResult = { added: 0, updated: 0 };
if (gossipResponse.handles && gossipResponse.handles.length > 0) { if (gossipResponse.handles && gossipResponse.handles.length > 0) {
handlesResult = await upsertHandleEntries(gossipResponse.handles); // PASS TARGET: If we gossiped TO them, and they replied, treat their reply as authoritative
handlesResult = await upsertHandleEntries(gossipResponse.handles, targetDomain);
} }
await markNodeSuccess(targetDomain); await markNodeSuccess(targetDomain);
+65
View File
@@ -0,0 +1,65 @@
import { db, handleRegistry, users } from '@/db';
import { eq } from 'drizzle-orm';
import { gossipToNode } from './gossip';
import { fetchSwarmUserProfile, fetchSwarmPost } from './interactions';
import { upsertHandleEntries } from '@/lib/federation/handles';
/**
* Attempt to "heal" a connection to a user by finding their correct node
* and syncing with it.
*
* @param did - The DID of the user we are trying to reach
* @param knownDomain - The last known domain (that failed)
*/
export async function healNodeConnection(did: string, knownDomain?: string): Promise<boolean> {
console.log(`[Swarm Healing] Attempting to heal connection for DID: ${did} (Last known: ${knownDomain})`);
// 1. Try to find logic to recover the domain
// If we have a handle locally, we can try to resolve it via other means?
// Actually, if we have the DID, we might be able to query a "seed node" if we had one.
// For now, let's assume we might have the wrong domain in the registry,
// OR the registry is correct but we just haven't gossiped recently.
// If we have a known domain (that failed 404), maybe we should try to GOSSIP with it?
// Sometimes a node knows about itself but we haven't synced it.
if (knownDomain) {
console.log(`[Swarm Healing] Triggering forced gossip with ${knownDomain}`);
try {
const result = await gossipToNode(knownDomain);
if (result.success && result.handlesReceived > 0) {
console.log(`[Swarm Healing] Gossip successful, handles updated.`);
// Check if our specific user was updated?
// We trust upsertHandleEntries handled it if authoritative.
return true;
}
} catch (e) {
console.error(`[Swarm Healing] Gossip to ${knownDomain} failed:`, e);
}
}
// 2. Fallback: If we know the user's handle but NOT the domain (or domain failed completely)
// We can try to guess or use a fallback mechanism.
// Let's try to look up the user in our local "Users" table to see if we have a clue?
// Or assume the formatting of the DID might give a hint? (Did is meaningless usually).
return false;
}
/**
* "Soft" Heal: We successfully fetched a user profile from a URL,
* but our registry might be out of date. Update it immediately.
*/
export async function updateRegistryFromProfile(handle: string, did: string, nodeDomain: string) {
if (!handle || !did || !nodeDomain) return;
console.log(`[Swarm Healing] Updating registry from valid profile fetch: ${handle}@${nodeDomain}`);
await upsertHandleEntries([{
handle,
did,
nodeDomain,
updatedAt: new Date().toISOString()
}], nodeDomain);
}