Refactor chat system to remove E2EE and simplify messaging

Removed all E2EE chat endpoints, crypto logic, and related API routes, transitioning chat to plain text storage and transport. Updated chat send/receive endpoints to use signed actions and enforce DM privacy settings. Cleaned up Swarm chat inbox, key management, and related debug endpoints. Updated user profile to support DM privacy, improved search for handle queries, and refactored conversation/message logic to support the new model. Migrated bot settings and privacy settings to new locations.
This commit is contained in:
Christopher
2026-01-28 13:05:34 -08:00
parent b52b3f9804
commit ceae76d58f
44 changed files with 1945 additions and 3058 deletions
+107 -60
View File
@@ -3,108 +3,155 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { chatConversations, chatMessages, users, handleRegistry } from '@/db/schema';
import { eq, and } from 'drizzle-orm';
import { verifyActionSignature, type SignedAction } from '@/lib/auth/verify-signature';
/**
* POST /api/chat/receive
* Endpoint for receiving federated chat messages from other nodes.
*
* Body: {
* senderDid: string,
* senderHandle: string, // user@domain
* senderDisplayName: string,
* senderAvatarUrl: string,
* senderNodeDomain: string,
* recipientDid: string,
* encryptedContent: string (base64 JSON of {senderPublicKey, ciphertext, nonce, recipientDid}),
* sentAt: string (ISO date)
* }
* Expects a SignedAction payload from the sender.
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
senderDid,
senderHandle,
senderDisplayName,
senderAvatarUrl,
senderNodeDomain,
recipientDid,
encryptedContent
} = body;
const signedAction: SignedAction = await request.json();
const { did, handle, data } = signedAction;
const { recipientDid, content } = data || {};
// Basic validation
if (!senderDid || !senderHandle || !recipientDid || !encryptedContent || !senderNodeDomain) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
if (!did || !handle || !recipientDid || !content) {
return NextResponse.json({ error: 'Invalid payload' }, { status: 400 });
}
console.log(`[Chat Receive] From: ${senderHandle} (${senderNodeDomain}), To: ${recipientDid} [${encryptedContent.substring(0, 20)}...]`);
console.log(`[Chat Receive] From: ${handle} (DID: ${did}), To: ${recipientDid}`);
// Ensure remote handle is fully qualified
let finalSenderHandle = senderHandle;
if (!finalSenderHandle.includes('@') && senderNodeDomain) {
finalSenderHandle = `${senderHandle}@${senderNodeDomain}`;
// 1. Resolve Sender Public Key
let senderUser = await db.query.users.findFirst({
where: eq(users.did, did)
});
let publicKey = senderUser?.publicKey;
let senderDisplayName = senderUser?.displayName || handle;
let senderAvatarUrl = senderUser?.avatarUrl;
let senderNodeDomain: string | null = null;
if (!senderUser) {
// Unknown user - likely remote. We need to fetch their profile to get the public key.
// Derive domain from handle if possible
if (handle.includes('@')) {
const parts = handle.split('@');
senderNodeDomain = parts[parts.length - 1];
} else {
// Try handle registry (though we likely don't have it if we don't have the user)
const registryEntry = await db.query.handleRegistry.findFirst({
where: eq(handleRegistry.did, did)
});
if (registryEntry) senderNodeDomain = registryEntry.nodeDomain;
}
if (senderNodeDomain) {
try {
const protocol = senderNodeDomain.includes('localhost') ? 'http' : 'https';
// Fetch profile from remote node
// Assuming /api/users/:handle convention
const remoteHandle = handle.includes('@') ? handle.split('@')[0] : handle;
const res = await fetch(`${protocol}://${senderNodeDomain}/api/users/${remoteHandle}`);
if (res.ok) {
const profileData = await res.json();
const remoteProfile = profileData.user;
if (remoteProfile && remoteProfile.publicKey) {
if (remoteProfile.did !== did) {
console.error('DID mismatch for remote user');
} else {
publicKey = remoteProfile.publicKey;
senderDisplayName = remoteProfile.displayName || handle;
senderAvatarUrl = remoteProfile.avatarUrl;
}
}
} else {
console.error('Failed to fetch remote profile:', res.status);
}
} catch (e) {
console.error('Remote profile fetch failed:', e);
}
}
}
// 1. Find local recipient
if (!publicKey) {
return NextResponse.json({ error: 'Could not resolve sender public key' }, { status: 401 });
}
// 2. Verify Signature
const isValid = await verifyActionSignature(signedAction, publicKey);
if (!isValid) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 403 });
}
// 3. Find Local Recipient
const recipientUser = await db.query.users.findFirst({
where: eq(users.did, recipientDid)
});
if (!recipientUser) {
return NextResponse.json({ error: 'Recipient not found' }, { status: 404 });
return NextResponse.json({ error: 'Recipient not found on this node' }, { status: 404 });
}
// 2. Find or Create Conversation
// For the RECIPIENT, the conversation is with the SENDER (Remote)
// 4. Find or Create Conversation
// For the RECIPIENT, the conversation is with the SENDER
// Use full handle
const fullSenderHandle = handle.includes('@') ? handle : (senderNodeDomain ? `${handle}@${senderNodeDomain}` : handle);
let conversation = await db.query.chatConversations.findFirst({
where: and(
eq(chatConversations.participant1Id, recipientUser.id),
eq(chatConversations.participant2Handle, finalSenderHandle)
eq(chatConversations.participant2Handle, fullSenderHandle)
)
});
if (!conversation) {
const [newConv] = await db.insert(chatConversations).values({
participant1Id: recipientUser.id,
participant2Handle: finalSenderHandle,
participant2Handle: fullSenderHandle,
lastMessageAt: new Date(),
lastMessagePreview: '[Encrypted Message]'
lastMessagePreview: content.slice(0, 50)
}).returning();
conversation = newConv;
} else {
// Update preview
await db.update(chatConversations)
.set({
lastMessageAt: new Date(),
lastMessagePreview: content.slice(0, 50),
updatedAt: new Date()
})
.where(eq(chatConversations.id, conversation.id));
}
// 3. Store Message
// 5. Store Message
await db.insert(chatMessages).values({
conversationId: conversation.id,
senderHandle: finalSenderHandle,
senderDisplayName: senderDisplayName || senderHandle,
senderHandle: fullSenderHandle,
senderDisplayName: senderDisplayName,
senderAvatarUrl: senderAvatarUrl,
senderNodeDomain: senderNodeDomain,
senderDid: senderDid,
encryptedContent: encryptedContent,
senderDid: did,
content: content,
encryptedContent: '',
deliveredAt: new Date(),
});
// 4. Update conversation timestamp
await db.update(chatConversations)
.set({
lastMessageAt: new Date(),
lastMessagePreview: '[Encrypted Message]'
})
.where(eq(chatConversations.id, conversation.id));
// 5. Upsert sender into HandleRegistry ensures we can reply/fetch keys later
await db.insert(handleRegistry).values({
handle: finalSenderHandle, // user@domain
did: senderDid,
nodeDomain: senderNodeDomain
}).onConflictDoUpdate({
target: handleRegistry.handle,
set: {
did: senderDid,
// 6. Update Registry (to ensure we can reply efficiently)
if (senderNodeDomain) {
await db.insert(handleRegistry).values({
handle: fullSenderHandle, // user@domain
did: did,
nodeDomain: senderNodeDomain
}
});
}).onConflictDoUpdate({
target: handleRegistry.handle,
set: {
did: did,
nodeDomain: senderNodeDomain
}
});
}
return NextResponse.json({ success: true });