import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; interface AssetExpandQuery { set?: string; } async function fetchWithRetry(url: string, retries = 1, timeout = 10000): Promise { for (let i = 0; i <= retries; i++) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { if (i === retries) throw new Error(`HTTP ${response.status}`); continue; } return await response.json(); } catch (e) { clearTimeout(timeoutId); if (i === retries) throw e; await new Promise(r => setTimeout(r, 1500)); } } } export async function assetExpandRoutes(fastify: FastifyInstance): Promise { fastify.get<{ Querystring: AssetExpandQuery }>( '/asset-expand', async (request: FastifyRequest<{ Querystring: AssetExpandQuery }>, reply: FastifyReply) => { const setName = request.query.set || ''; reply.header('Access-Control-Allow-Origin', '*'); reply.header('Cache-Control', 'public, max-age=3600'); if (!setName) { return reply.status(400).send({ error: 'set required (e.g. AS-FLEXOPTIX)' }); } async function expandSet(name: string, depth: number, visited: Set): Promise<{ asns: string[], sets: string[] }> { if (depth > 4 || visited.has(name)) return { asns: [], sets: [] }; visited.add(name); const url = `https://rest.db.ripe.net/search.json?query-string=${encodeURIComponent(name)}&type-filter=as-set&flags=no-referenced`; let data; try { data = await fetchWithRetry(url); } catch (e) { // If RIPE DB fails on a specific set, gracefully continue with empty array for that branch return { asns: [], sets: [] }; } const asns: string[] = []; const sets: string[] = []; if (data && data.objects && data.objects.object) { for (const obj of data.objects.object) { const attrs = (obj.attributes && obj.attributes.attribute) || []; for (const a of attrs) { if (a.name === 'members') { for (const m of (a.value || '').split(/[,\s]+/).filter(Boolean)) { if (/^AS\d+$/i.test(m)) asns.push(m.toUpperCase()); else if (m.startsWith('AS-')) sets.push(m); } } } } } for (const sub of sets.slice(0, 10)) { // Limit sub-sets to prevent overwhelming API const sub_r = await expandSet(sub, depth + 1, visited); asns.push(...sub_r.asns); } return { asns: [...new Set(asns)], sets }; } try { const visited = new Set(); const result = await expandSet(setName.toUpperCase(), 0, visited); result.asns.sort((a, b) => parseInt(a.slice(2)) - parseInt(b.slice(2))); return reply.status(200).send({ set: setName.toUpperCase(), count: result.asns.length, asns: result.asns, sub_sets: result.sets }); } catch (e: any) { return reply.status(500).send({ error: e.message }); } } ); }