import { NextRequest, NextResponse } from 'next/server'; import { kuboApiUrl, kuboBasicAuth } from '@/lib/config'; import { localListPins, localAddPin, localRemovePin } from '@/lib/local-pins'; export async function proxyIPFS(path: string, method: string, req: NextRequest): Promise { // Normalize path const p = path.replace(/^\/+/, '').replace(/\/+$/, ''); // ── Lokale pin opslag (zonder Kubo) ── // Wanneer KUBO_API_URL niet beschikbaar is, gebruik lokale JSON file if (p === 'pins' || p.startsWith('pins/')) { const kuboSvc = kuboApiUrl(); if (!kuboSvc) { return handleLocalPins(p, method, req); } } // In dev, the Kubo API is behind nginx with Basic Auth. // If no KUBO_API_URL is configured, return a clear error. const kuboSvc = kuboApiUrl(); if (!kuboSvc) { return NextResponse.json( { error: 'IPFS proxy not configured', detail: 'Set KUBO_API_URL in env for dev, or use nginx routing in production' }, { status: 501 } ); } // Map /api/explorer/ls/ → /api/v0/ls?arg=, etc. const url = new URL(kuboSvc); const kuboPath = mapToKuboAPI(path, method); // Split pathname and query string — url.pathname encodes `?` as `%3F` const [namePart, ...qsParts] = kuboPath.split('?'); url.pathname = namePart; if (qsParts.length > 0) { url.search = qsParts.join('?'); } else { // Forward incoming query params (used by ipns/publish etc.) const incomingSearch = req.nextUrl.search; if (incomingSearch) { url.search = incomingSearch; } } const headers: Record = {}; const auth = kuboBasicAuth(); if (auth) { headers['Authorization'] = 'Basic ' + Buffer.from(auth).toString('base64'); } // Forward Content-Type (preserves multipart boundary for file uploads) const reqCt = req.headers.get('content-type'); if (reqCt) { headers['Content-Type'] = reqCt; } // Kubo uses POST for everything const body = method === 'POST' || method === 'DELETE' ? req.body : null; const fetchOpts: RequestInit & { duplex?: string } = { method: 'POST', headers, signal: AbortSignal.timeout(30000), }; // Node.js fetch requires duplex: 'half' when streaming a body if (body) { fetchOpts.duplex = 'half'; fetchOpts.body = body; } let res: Response; try { res = await fetch(url.toString(), fetchOpts); } catch (fetchErr: unknown) { const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr); console.error('[proxyIPFS] fetch error:', msg); return NextResponse.json( { error: 'IPFS proxy fetch failed', detail: msg.substring(0, 200) }, { status: 502 }, ); } const text = await res.text(); return new NextResponse(text, { status: res.status, headers: { 'Content-Type': res.headers.get('Content-Type') || 'application/json', }, }); } /** Lokale pin handler — werkt zonder Kubo API */ async function handleLocalPins(path: string, method: string, req: NextRequest): Promise { const p = path.replace(/^\/+/, '').replace(/\/+$/, ''); // GET /api/pins — lijst alle pins if (p === 'pins' && method === 'GET') { const pins = await localListPins(); // Retourneer in Kubo-formaat voor backwards compatibiliteit const Keys: Record = {}; for (const pin of pins) { Keys[pin.cid] = { Type: pin.created || 'indirect' }; } return NextResponse.json({ Keys }); } // POST /api/pins — pin een CID if (p === 'pins' && method === 'POST') { try { const body = await req.json(); const { cid, name, size } = body; if (!cid) return NextResponse.json({ error: 'cid required' }, { status: 400 }); await localAddPin(cid, name, size || 0); return NextResponse.json({ cid }); } catch { return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); } } // DELETE /api/pins/ — verwijder pin if (p.startsWith('pins/') && method === 'DELETE') { const cid = p.slice(5); await localRemovePin(cid); return NextResponse.json({ ok: true }); } return NextResponse.json({ error: 'Not found' }, { status: 404 }); } export function mapToKuboAPI(path: string, method: string): string { // Normalize: /api/node/info → /api/v0/id, etc. const p = path.replace(/^\/+/, '').replace(/\/+$/, ''); switch (p) { case 'node/info': return '/api/v0/id'; case 'peers': return '/api/v0/swarm/peers'; case 'pins': return method === 'GET' ? '/api/v0/pin/ls' : '/api/v0/pin/add'; default: if (p.startsWith('pins/')) { const cid = p.slice(5); return `/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`; } if (p === 'files/upload') { return '/api/v0/add?pin=true'; } if (p.startsWith('files/')) { return '/api/v0/ls'; } // Explorer routes: /explorer/ls/, /explorer/cat/, etc. if (p.startsWith('explorer/ls/')) { const cid = p.slice('explorer/ls/'.length); return `/api/v0/ls?arg=${encodeURIComponent(cid)}`; } if (p.startsWith('explorer/cat/')) { const cid = p.slice('explorer/cat/'.length); return `/api/v0/cat?arg=${encodeURIComponent(cid)}`; } if (p.startsWith('explorer/stat/')) { const cid = p.slice('explorer/stat/'.length); return `/api/v0/object/stat?arg=${encodeURIComponent(cid)}`; } if (p.startsWith('explorer/resolve/')) { const name = p.slice('explorer/resolve/'.length); return `/api/v0/resolve?arg=${encodeURIComponent(name)}`; } // IPNS routes if (p === 'ipns/publish') return '/api/v0/name/publish'; if (p === 'ipns/keys') return '/api/v0/key/list'; if (p.startsWith('ipns/resolve/')) { const name = p.slice('ipns/resolve/'.length); return `/api/v0/name/resolve?arg=${encodeURIComponent(name)}`; } if (p.startsWith('ipns/keys/gen/')) { const name = p.slice('ipns/keys/gen/'.length); return `/api/v0/key/gen?arg=${encodeURIComponent(name)}`; } // Dashboard routes if (p === 'repo/stats') return '/api/v0/repo/stat'; if (p === 'bw/stats') return '/api/v0/bw'; return '/api/v0/' + p; } }