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:
Christopher
2026-01-27 20:36:03 -08:00
parent 8c3dffaa4c
commit e92c747a5b
7 changed files with 328 additions and 29 deletions
+18 -6
View File
@@ -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
View File
@@ -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);