From 74d61474a67d2b5ecc72ccdfb535153b0c490ab3 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Fri, 31 Jul 2026 23:23:46 -0600 Subject: [PATCH 1/2] fix(chat): trim participant conversation ids --- .../conversations/[id]/participants/route.js | 3 ++- .../[id]/participants/route.test.js | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/app/api/chat/conversations/[id]/participants/route.js b/src/app/api/chat/conversations/[id]/participants/route.js index 5c439a96..0292fcdf 100644 --- a/src/app/api/chat/conversations/[id]/participants/route.js +++ b/src/app/api/chat/conversations/[id]/participants/route.js @@ -123,7 +123,8 @@ export async function GET(request, { params } = {}) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const { id: conversationId } = await resolveRouteParams(params); + const { id: rawConversationId } = await resolveRouteParams(params); + const conversationId = typeof rawConversationId === 'string' ? rawConversationId.trim() : ''; if (!conversationId) { return NextResponse.json({ error: 'Missing conversation ID' }, { status: 400 }); } diff --git a/src/app/api/chat/conversations/[id]/participants/route.test.js b/src/app/api/chat/conversations/[id]/participants/route.test.js index bcc31e41..338be74f 100644 --- a/src/app/api/chat/conversations/[id]/participants/route.test.js +++ b/src/app/api/chat/conversations/[id]/participants/route.test.js @@ -129,4 +129,25 @@ describe('GET /api/chat/conversations/[id]/participants', () => { expect(body).toEqual({ error: 'Missing conversation ID' }); expect(mocks.authGetUser).toHaveBeenCalled(); }); + + it('rejects whitespace-only conversation IDs before participant lookup', async () => { + mocks.serviceFrom.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + if (table === 'conversation_participants') throw new Error('Participant lookup should not run'); + throw new Error(`Unexpected table: ${table}`); + }); + + const { GET } = await import('./route.js'); + const response = await GET( + new Request('https://qrypt.chat/api/chat/conversations/%20%20/participants', { + headers: { cookie: 'session=header.payload.signature' } + }), + { params: Promise.resolve({ id: ' ' }) } + ); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body).toEqual({ error: 'Missing conversation ID' }); + expect(mocks.authGetUser).toHaveBeenCalled(); + }); }); From a1ade336e7b4bafeb6c01af4bd4d8982d20b74fd Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sun, 2 Aug 2026 10:56:49 -0600 Subject: [PATCH 2/2] fix(crypto): bound public key batch lookups --- src/app/api/crypto/public-keys/route.js | 18 +++++++++- src/app/api/crypto/public-keys/route.test.js | 35 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/app/api/crypto/public-keys/route.js b/src/app/api/crypto/public-keys/route.js index 29166cbe..52b3357b 100644 --- a/src/app/api/crypto/public-keys/route.js +++ b/src/app/api/crypto/public-keys/route.js @@ -151,6 +151,8 @@ export async function GET(request) { */ export async function POST(request) { try { + const MAX_BATCH_SIZE = 100; + // Authenticate user const { user, error: authError } = await authenticateUser(request); if (authError || !user) { @@ -163,11 +165,25 @@ export async function POST(request) { return NextResponse.json({ error: 'Missing or invalid user_ids array' }, { status: 400 }); } + if (user_ids.length > MAX_BATCH_SIZE) { + return NextResponse.json({ error: `user_ids must contain at most ${MAX_BATCH_SIZE} items` }, { status: 400 }); + } + + const normalizedUserIds = [...new Set(user_ids.map((userId) => { + if (typeof userId !== 'string') return null; + const normalized = userId.trim(); + return normalized || null; + }))]; + + if (normalizedUserIds.includes(null)) { + return NextResponse.json({ error: 'user_ids must contain non-empty strings' }, { status: 400 }); + } + /** @type {Record} */ const publicKeys = {}; // Process each user ID - for (const userId of user_ids) { + for (const userId of normalizedUserIds) { try { // Get the user's auth_user_id from the internal user ID const { data: userData, error: userError } = await getServiceRoleClient() diff --git a/src/app/api/crypto/public-keys/route.test.js b/src/app/api/crypto/public-keys/route.test.js index 9e021418..5d9667ba 100644 --- a/src/app/api/crypto/public-keys/route.test.js +++ b/src/app/api/crypto/public-keys/route.test.js @@ -103,4 +103,39 @@ describe('public key cookie authentication', () => { expect(body).toEqual({ error: 'Missing public_key' }); expect(mocks.rpc).not.toHaveBeenCalled(); }); + + it('rejects oversized public-key batches before service-role queries', async () => { + const { POST } = await import('./route.js'); + const response = await POST( + new Request('https://qrypt.chat/api/crypto/public-keys', { + method: 'POST', + headers: { + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('access-token')}`, + 'content-type': 'application/json' + }, + body: JSON.stringify({ user_ids: Array.from({ length: 101 }, (_, index) => `user-${index}`) }) + }) + ); + + expect(response.status).toBe(400); + expect(mocks.serviceFrom).not.toHaveBeenCalled(); + }); + + it('trims and deduplicates user IDs before lookup', async () => { + const { POST } = await import('./route.js'); + const response = await POST( + new Request('https://qrypt.chat/api/crypto/public-keys', { + method: 'POST', + headers: { + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('access-token')}`, + 'content-type': 'application/json' + }, + body: JSON.stringify({ user_ids: [' target-user-id ', 'target-user-id'] }) + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ public_keys: { 'target-user-id': 'public-key' } }); + expect(mocks.serviceFrom).toHaveBeenCalledTimes(1); + }); });