From a16f22ed341dca1b88fdf627bc72bf20387e0a50 Mon Sep 17 00:00:00 2001 From: BigBodyCobain <43977454+BigBodyCobain@users.noreply.github.com> Date: Fri, 29 May 2026 08:15:06 -0600 Subject: [PATCH] Cover AI and SAR proxy auth routes --- .../proxy/proxyAdminKeyInjection.test.ts | 95 ++++++++++++++++++- frontend/src/app/api/[...path]/route.ts | 5 +- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/frontend/src/__tests__/proxy/proxyAdminKeyInjection.test.ts b/frontend/src/__tests__/proxy/proxyAdminKeyInjection.test.ts index 006ee20..57c20bf 100644 --- a/frontend/src/__tests__/proxy/proxyAdminKeyInjection.test.ts +++ b/frontend/src/__tests__/proxy/proxyAdminKeyInjection.test.ts @@ -7,7 +7,7 @@ * - /api/tools/* (Sprint 1C addition) * - /api/wormhole/* (pre-existing, regression) * - /api/settings/* (pre-existing, regression) - * - /api/layers, /api/ais/feed, /api/ai/agent-actions + * - /api/layers, /api/ais/feed, /api/ai/*, /api/sar/mode-b/* * * Also verifies that: * - non-sensitive mesh paths (e.g. mesh/events) do NOT receive injected key @@ -344,6 +344,99 @@ describe('proxy admin-key injection coverage', () => { expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY); }); + it('GET /api/ai/pins with valid session injects X-Admin-Key', async () => { + const cookie = await mintSession(ADMIN_KEY); + + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ ok: true, pins: [] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + const req = new NextRequest('http://localhost/api/ai/pins?limit=500', { + method: 'GET', + headers: { cookie }, + }); + const res = await proxyGet(req, { + params: Promise.resolve({ path: ['ai', 'pins'] }), + }); + + expect(res.status).toBe(200); + expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY); + }); + + it('GET /api/ai/layers with valid session injects X-Admin-Key', async () => { + const cookie = await mintSession(ADMIN_KEY); + + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ ok: true, layers: [] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + const req = new NextRequest('http://localhost/api/ai/layers', { + method: 'GET', + headers: { cookie }, + }); + const res = await proxyGet(req, { + params: Promise.resolve({ path: ['ai', 'layers'] }), + }); + + expect(res.status).toBe(200); + expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY); + }); + + it('GET /api/ai/timemachine/config with valid session injects X-Admin-Key', async () => { + const cookie = await mintSession(ADMIN_KEY); + + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ ok: true, config: {} }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + const req = new NextRequest('http://localhost/api/ai/timemachine/config', { + method: 'GET', + headers: { cookie }, + }); + const res = await proxyGet(req, { + params: Promise.resolve({ path: ['ai', 'timemachine', 'config'] }), + }); + + expect(res.status).toBe(200); + expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY); + }); + + it('POST /api/sar/mode-b/enable with valid session injects X-Admin-Key', async () => { + const cookie = await mintSession(ADMIN_KEY); + + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + const req = new NextRequest('http://localhost/api/sar/mode-b/enable', { + method: 'POST', + body: JSON.stringify({ earthdata_user: 'operator', earthdata_token: 'token' }), + headers: { cookie, 'Content-Type': 'application/json' }, + }); + const res = await proxyPost(req, { + params: Promise.resolve({ path: ['sar', 'mode-b', 'enable'] }), + }); + + expect(res.status).toBe(200); + expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY); + }); + // ------------------------------------------------------------------------- // Non-sensitive mesh paths must NOT receive injected admin key // ------------------------------------------------------------------------- diff --git a/frontend/src/app/api/[...path]/route.ts b/frontend/src/app/api/[...path]/route.ts index 0c8ee33..a29cadd 100644 --- a/frontend/src/app/api/[...path]/route.ts +++ b/frontend/src/app/api/[...path]/route.ts @@ -66,7 +66,10 @@ function isSensitiveProxyPath(pathSegments: string[]): boolean { if (joined === 'system/update') return true; if (joined === 'layers') return true; if (joined === 'ais/feed') return true; - if (joined === 'ai/agent-actions') return true; + if (pathSegments[0] === 'ai') return true; + if (pathSegments[0] === 'sar' && (pathSegments[1] === 'mode-b' || pathSegments[1] === 'aois')) { + return true; + } if (pathSegments[0] === 'settings') return true; if (joined === 'mesh/infonet/ingest') return true; if (joined === 'mesh/meshtastic/send') return true;