Enhance chat key lookup with remote and cache fallback

The GET /api/chat/keys endpoint now checks a remote identity cache and, if necessary, fetches keys from remote nodes or Swarm user profiles when a local key is not found. Results from remote lookups are cached for 24 hours to improve performance and reliability.
This commit is contained in:
Christopher
2026-01-28 06:45:28 -08:00
parent 4ab464bcaa
commit 213dcc8095
+118 -11
View File
@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { chatDeviceBundles } from '@/db/schema';
import { chatDeviceBundles, handleRegistry, remoteIdentityCache } from '@/db/schema';
import { requireAuth } from '@/lib/auth';
import { eq, and } from 'drizzle-orm';
import { eq, and, gt } from 'drizzle-orm';
/**
* GET /api/chat/keys?did=<did>
@@ -23,7 +23,7 @@ export async function GET(request: NextRequest) {
where: eq(chatDeviceBundles.did, did),
});
console.log('[Chat Keys GET] Found bundle:', bundle ? 'YES' : 'NO');
console.log('[Chat Keys GET] Found local bundle:', bundle ? 'YES' : 'NO');
if (bundle) {
console.log('[Chat Keys GET] Bundle data:', {
userId: bundle.userId,
@@ -31,16 +31,123 @@ export async function GET(request: NextRequest) {
deviceId: bundle.deviceId,
hasIdentityKey: !!bundle.identityKey,
});
// For libsodium, we just need the public key
return NextResponse.json({
publicKey: bundle.identityKey,
});
}
if (!bundle) {
return NextResponse.json({ error: 'No keys found' }, { status: 404 });
}
// For libsodium, we just need the public key
return NextResponse.json({
publicKey: bundle.identityKey, // Reusing identityKey field for libsodium public key
// If not found locally, check remote identity cache
const cached = await db.query.remoteIdentityCache.findFirst({
where: and(
eq(remoteIdentityCache.did, did),
gt(remoteIdentityCache.expiresAt, new Date())
),
});
if (cached) {
console.log('[Chat Keys GET] Found cached remote key');
return NextResponse.json({
publicKey: cached.publicKey,
});
}
// If not in cache, try to resolve DID to a node
console.log('[Chat Keys GET] resolving DID to node...');
const handleEntry = await db.query.handleRegistry.findFirst({
where: eq(handleRegistry.did, did),
});
if (handleEntry) {
const { nodeDomain } = handleEntry;
console.log('[Chat Keys GET] Resolved to node:', nodeDomain);
// Fetch from remote node
const remoteUrl = `https://${nodeDomain}/api/chat/keys?did=${encodeURIComponent(did)}`;
console.log('[Chat Keys GET] Fetching from remote:', remoteUrl);
try {
const remoteRes = await fetch(remoteUrl);
if (remoteRes.ok) {
const data = await remoteRes.json();
if (data.publicKey) {
console.log('[Chat Keys GET] Successfully fetched remote key');
// Cache it
await db.insert(remoteIdentityCache).values({
did: did,
publicKey: data.publicKey,
fetchedAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
}).onConflictDoUpdate({
target: remoteIdentityCache.did,
set: {
publicKey: data.publicKey,
fetchedAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
}
});
return NextResponse.json({
publicKey: data.publicKey,
});
}
} else {
console.warn('[Chat Keys GET] Remote fetch failed:', remoteRes.status, remoteRes.statusText);
}
} catch (err) {
console.error('[Chat Keys GET] Remote fetch error:', err);
}
// FALLBACK: Try fetching from Swarm User Profile (standard endpoint)
if (!cached) {
console.log('[Chat Keys GET] Trying fallback to Swarm Profile...');
const swarmUrl = `https://${nodeDomain}/api/swarm/users/${handleEntry.handle}`;
console.log('[Chat Keys GET] Fetching Swarm Profile:', swarmUrl);
try {
const swarmRes = await fetch(swarmUrl);
if (swarmRes.ok) {
const data = await swarmRes.json();
const chatKey = data.profile?.chatPublicKey;
if (chatKey) {
console.log('[Chat Keys GET] Found key in Swarm Profile');
// Cache it
await db.insert(remoteIdentityCache).values({
did: did,
publicKey: chatKey,
fetchedAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
}).onConflictDoUpdate({
target: remoteIdentityCache.did,
set: {
publicKey: chatKey,
fetchedAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
}
});
return NextResponse.json({
publicKey: chatKey,
});
} else {
console.warn('[Chat Keys GET] Swarm Profile found but no chatPublicKey');
}
} else {
console.warn('[Chat Keys GET] Swarm Profile fetch failed:', swarmRes.status);
}
} catch (err) {
console.error('[Chat Keys GET] Swarm Profile fetch error:', err);
}
}
} else {
console.log('[Chat Keys GET] DID not found in handle registry');
}
return NextResponse.json({ error: 'No keys found' }, { status: 404 });
} catch (error: any) {
console.error('[Chat Keys] Failed to fetch keys:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
@@ -58,7 +165,7 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const user = await requireAuth();
console.log('[Chat Keys POST] User:', user.handle, 'DID:', user.did, 'UserID:', user.id);
const body = await request.json();