diff --git a/src/app/api/crypto/public-keys/route.js b/src/app/api/crypto/public-keys/route.js index adc35c8..8c21314 100644 --- a/src/app/api/crypto/public-keys/route.js +++ b/src/app/api/crypto/public-keys/route.js @@ -17,6 +17,27 @@ function getServiceRoleClient() { // Create regular client for JWT validation const supabaseClient = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY); +export const MAX_PUBLIC_KEY_BATCH_SIZE = 100; + +export function normalizePublicKeyUserIds(value) { + if (!Array.isArray(value) || value.length > MAX_PUBLIC_KEY_BATCH_SIZE) { + return null; + } + + const userIds = []; + const seen = new Set(); + for (const candidate of value) { + if (typeof candidate !== 'string') return null; + const userId = candidate.trim(); + if (!userId || userId.length > 128) return null; + if (seen.has(userId)) continue; + seen.add(userId); + userIds.push(userId); + } + + return userIds; +} + /** * Authenticate user from request cookies * @param {Request} request @@ -159,17 +180,26 @@ export async function POST(request) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const { user_ids } = await request.json(); - - if (!user_ids || !Array.isArray(user_ids)) { - return NextResponse.json({ error: 'Missing or invalid user_ids array' }, { status: 400 }); + let body; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const userIds = normalizePublicKeyUserIds(body?.user_ids); + if (!userIds) { + return NextResponse.json( + { error: `user_ids must contain at most ${MAX_PUBLIC_KEY_BATCH_SIZE} non-empty strings` }, + { status: 400 } + ); } - /** @type {Record} */ - const publicKeys = {}; + /** @type {Map} */ + const publicKeys = new Map(); // Process each user ID - for (const userId of user_ids) { + for (const userId of userIds) { try { // Get the user's auth_user_id from the internal user ID const { data: userData, error: userError } = await getServiceRoleClient() @@ -179,8 +209,8 @@ export async function POST(request) { .single(); if (userError || !userData?.auth_user_id) { - console.log(`🔑 No auth_user_id found for internal user ${userId}`); - publicKeys[userId] = null; + console.log('No auth_user_id found for requested public key'); + publicKeys.set(userId, null); continue; } @@ -192,19 +222,19 @@ export async function POST(request) { }); if (error) { - console.error(`Error fetching public key for user ${userId}:`, error); - publicKeys[userId] = null; + console.error('Error fetching requested public key:', error); + publicKeys.set(userId, null); } else { - publicKeys[userId] = publicKey; + publicKeys.set(userId, publicKey); } } catch (error) { - console.error(`Error processing user ${userId}:`, error); - publicKeys[userId] = null; + console.error('Error processing public key lookup:', error); + publicKeys.set(userId, null); } } - return NextResponse.json({ public_keys: publicKeys }); + return NextResponse.json({ public_keys: Object.fromEntries(publicKeys) }); } catch (error) { console.error('Error in POST /api/crypto/public-keys:', error); diff --git a/src/app/api/crypto/public-keys/route.test.js b/src/app/api/crypto/public-keys/route.test.js index 74987a8..e7836ce 100644 --- a/src/app/api/crypto/public-keys/route.test.js +++ b/src/app/api/crypto/public-keys/route.test.js @@ -121,4 +121,77 @@ describe('public key cookie authentication', () => { expect(body).toEqual({ error: 'Missing public_key' }); expect(mocks.rpc).not.toHaveBeenCalled(); }); + + it('rejects oversized public key batches before lookup work', 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}`) + }) + }) + ); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toContain('at most 100'); + expect(mocks.serviceFrom).toHaveBeenCalledTimes(1); + expect(mocks.rpc).not.toHaveBeenCalled(); + }); + + it('rejects malformed and invalid public key batches', async () => { + const { POST } = await import('./route.js'); + const headers = { + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('access-token')}`, + 'content-type': 'application/json' + }; + const malformedResponse = await POST( + new Request('https://qrypt.chat/api/crypto/public-keys', { + method: 'POST', + headers, + body: '{"user_ids":' + }) + ); + const invalidResponse = await POST( + new Request('https://qrypt.chat/api/crypto/public-keys', { + method: 'POST', + headers, + body: JSON.stringify({ user_ids: ['valid-id', 42] }) + }) + ); + + expect(malformedResponse.status).toBe(400); + expect(invalidResponse.status).toBe(400); + expect(mocks.rpc).not.toHaveBeenCalled(); + }); + + it('trims and deduplicates public key batch ids', 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: [' user-one ', 'user-one', 'user-two'] }) + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ + public_keys: { + 'user-one': 'public-key', + 'user-two': 'public-key' + } + }); + expect(mocks.serviceFrom).toHaveBeenCalledTimes(3); + expect(mocks.rpc).toHaveBeenCalledTimes(2); + }); });