Improve chat encryption, delivery, and session handling
Adds robust session serialization/deserialization for ratchet state, session cleanup for corrupted data, and improved error handling in chat encryption hooks. Enhances chat delivery to support both local and remote recipients, ensures conversation creation, and adds timeouts to key fetches. Also introduces utilities for deleting and clearing encrypted session data from storage.
This commit is contained in:
@@ -12,14 +12,26 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const handle = searchParams.get('handle');
|
||||
|
||||
// Helper to fetch and check
|
||||
// Helper to fetch and check with timeout
|
||||
const tryFetch = async (url: string) => {
|
||||
console.log(`[Proxy] Fetching keys from: ${url}`);
|
||||
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||
if (res.ok) return await res.json();
|
||||
const text = await res.text();
|
||||
console.warn(`[Proxy] Fetch failed for ${url} (${res.status}): ${text}`);
|
||||
return null;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), 5000); // 5s timeout
|
||||
const res = await fetch(url, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(id);
|
||||
|
||||
if (res.ok) return await res.json();
|
||||
const text = await res.text();
|
||||
console.warn(`[Proxy] Fetch failed for ${url} (${res.status}): ${text}`);
|
||||
return null;
|
||||
} catch (err: any) {
|
||||
console.warn(`[Proxy] Fetch error for ${url}: ${err.name} - ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Try Primary DID
|
||||
|
||||
+121
-11
@@ -1,12 +1,14 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { chatInbox } from '@/db/schema';
|
||||
import { chatInbox, chatConversations, users } from '@/db/schema';
|
||||
import { requireSignedAction } from '@/lib/auth/verify-signature';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { signedFetch } from '@/lib/api/signed-fetch';
|
||||
|
||||
/**
|
||||
* POST /api/chat/send
|
||||
* Deliver an encrypted envelope to a local user's inbox.
|
||||
* Deliver an encrypted envelope to a local or remote user's inbox.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -20,22 +22,130 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Invalid action type' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { recipientDid, recipientDeviceId, ciphertext } = data;
|
||||
const { recipientDid, recipientDeviceId, ciphertext, nodeDomain, recipientHandle } = data;
|
||||
|
||||
if (!recipientDid || !ciphertext) {
|
||||
return NextResponse.json({ error: 'Missing required delivery fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 2. Insert into Inbox
|
||||
await db.insert(chatInbox).values({
|
||||
senderDid: user.did,
|
||||
recipientDid,
|
||||
recipientDeviceId: recipientDeviceId || null, // Null = broadcast/all? Or specific? V2.1 says per-device.
|
||||
envelope: JSON.stringify(body),
|
||||
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days TTL
|
||||
// 2. Check if recipient is local or remote
|
||||
const recipientUser = await db.query.users.findFirst({
|
||||
where: eq(users.did, recipientDid)
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
if (recipientUser) {
|
||||
// LOCAL RECIPIENT - Store in local inbox
|
||||
|
||||
// Ensure conversation exists for recipient
|
||||
let conversation = await db.query.chatConversations.findFirst({
|
||||
where: and(
|
||||
eq(chatConversations.participant1Id, recipientUser.id),
|
||||
eq(chatConversations.participant2Handle, user.handle)
|
||||
)
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
const [newConv] = await db.insert(chatConversations).values({
|
||||
participant1Id: recipientUser.id,
|
||||
participant2Handle: user.handle,
|
||||
lastMessageAt: new Date(),
|
||||
lastMessagePreview: '[Encrypted Message]'
|
||||
}).returning();
|
||||
conversation = newConv;
|
||||
}
|
||||
|
||||
// Ensure conversation exists for sender
|
||||
let senderConversation = await db.query.chatConversations.findFirst({
|
||||
where: and(
|
||||
eq(chatConversations.participant1Id, user.id),
|
||||
eq(chatConversations.participant2Handle, recipientUser.handle)
|
||||
)
|
||||
});
|
||||
|
||||
if (!senderConversation) {
|
||||
await db.insert(chatConversations).values({
|
||||
participant1Id: user.id,
|
||||
participant2Handle: recipientUser.handle,
|
||||
lastMessageAt: new Date(),
|
||||
lastMessagePreview: '[Encrypted Message]'
|
||||
});
|
||||
}
|
||||
|
||||
// Insert into local inbox
|
||||
await db.insert(chatInbox).values({
|
||||
senderDid: user.did,
|
||||
recipientDid,
|
||||
recipientDeviceId: recipientDeviceId || null,
|
||||
envelope: JSON.stringify(body),
|
||||
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, local: true });
|
||||
|
||||
} else {
|
||||
// REMOTE RECIPIENT - Forward to their node
|
||||
|
||||
let targetNodeDomain = nodeDomain;
|
||||
|
||||
if (!targetNodeDomain) {
|
||||
// Try to extract from DID
|
||||
if (recipientDid.startsWith('did:web:')) {
|
||||
targetNodeDomain = recipientDid.replace('did:web:', '');
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
error: 'Remote delivery requires node domain'
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
// Forward the signed envelope to the remote node
|
||||
const remoteUrl = `https://${targetNodeDomain}/api/swarm/chat/inbox`;
|
||||
|
||||
try {
|
||||
const response = await fetch(remoteUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error('[Chat Send] Remote delivery failed:', errorData);
|
||||
return NextResponse.json({
|
||||
error: 'Remote delivery failed',
|
||||
details: errorData
|
||||
}, { status: response.status });
|
||||
}
|
||||
|
||||
// Create local conversation for sender if we have recipient handle
|
||||
if (recipientHandle) {
|
||||
const existingConv = await db.query.chatConversations.findFirst({
|
||||
where: and(
|
||||
eq(chatConversations.participant1Id, user.id),
|
||||
eq(chatConversations.participant2Handle, recipientHandle)
|
||||
)
|
||||
});
|
||||
|
||||
if (!existingConv) {
|
||||
await db.insert(chatConversations).values({
|
||||
participant1Id: user.id,
|
||||
participant2Handle: recipientHandle,
|
||||
lastMessageAt: new Date(),
|
||||
lastMessagePreview: '[Encrypted Message]'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, remote: true });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[Chat Send] Remote delivery error:', error);
|
||||
return NextResponse.json({
|
||||
error: 'Failed to reach remote node',
|
||||
details: error.message
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Delivery failed:', error);
|
||||
|
||||
Reference in New Issue
Block a user