From 213dcc8095d818f89a55e9254c1d5c58dcfaf22d Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 28 Jan 2026 06:45:28 -0800 Subject: [PATCH] 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. --- src/app/api/chat/keys/route.ts | 129 ++++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 11 deletions(-) diff --git a/src/app/api/chat/keys/route.ts b/src/app/api/chat/keys/route.ts index 3c3d633..b252cf3 100644 --- a/src/app/api/chat/keys/route.ts +++ b/src/app/api/chat/keys/route.ts @@ -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= @@ -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();