PeerCortex/server/topology.js
Rene Fichtmueller a5912a3d0c refactor: extract whois.js, topology.js, bgp-he-net.js, resolve-as-names.js
fetchBgpHeNet moved as-is, not removed, despite being confirmed dead code
in practice (its only call site in /api/lookup is hardcoded to
Promise.resolve(null)) -- that's a "disabled pending re-enable" state, a
bigger behavioral decision than this extraction pass should make alone.

Also widened the smoke-test harness's volatile-path exclusions: the live
Cloudflare RPKI feed's ASPA object count ticks between runs (independent
of any code change here), and the field appears under three different
names (aspa_map.entries, aspa_adoption.total_objects, plus the derived
delta_from_previous) that weren't all covered by the existing aspa_objects
exclusion. Re-captured baseline accordingly.

Verified via smoke-test harness (28/28 match). server.js: 4827 -> 4575 lines.
2026-07-17 00:04:51 +02:00

50 lines
2.5 KiB
JavaScript

const { fetchRipeStatCached } = require("./services/ripe-stat");
// Feature 25: Topology / AS-Relationships
async function fetchTopology(targetAsn, depth) {
const maxDepth = Math.min(depth || 2, 3);
const nodes = new Map();
const edges = [];
async function fetchNeighboursForAsn(asn, currentDepth) {
if (nodes.has(asn) && nodes.get(asn).depth <= currentDepth) return;
const [data, overview] = await Promise.all([
fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + asn),
fetchRipeStatCached("https://stat.ripe.net/data/as-overview/data.json?resource=AS" + asn),
]);
const name = overview?.data?.holder || "";
const neighbours = data?.data?.neighbours || [];
const upstreams = neighbours.filter((n) => n.type === "left");
const downstreams = neighbours.filter((n) => n.type === "right");
const peers = neighbours.filter((n) => n.type === "uncertain" || n.type === "peer");
const nodeType = asn === targetAsn ? "target" : currentDepth === 1 ? "direct" : "indirect";
nodes.set(asn, { asn, name, type: nodeType, depth: currentDepth });
upstreams.forEach((n) => {
if (!nodes.has(n.asn)) nodes.set(n.asn, { asn: n.asn, name: n.as_name || "", type: "upstream", depth: currentDepth + 1 });
edges.push({ from: n.asn, to: asn, relationship: "provider-to-customer" });
});
downstreams.forEach((n) => {
if (!nodes.has(n.asn)) nodes.set(n.asn, { asn: n.asn, name: n.as_name || "", type: "downstream", depth: currentDepth + 1 });
edges.push({ from: asn, to: n.asn, relationship: "provider-to-customer" });
});
peers.slice(0, 10).forEach((n) => {
if (!nodes.has(n.asn)) nodes.set(n.asn, { asn: n.asn, name: n.as_name || "", type: "peer", depth: currentDepth + 1 });
edges.push({ from: asn, to: n.asn, relationship: "peer" });
});
if (currentDepth < maxDepth && upstreams.length > 0) {
const top5 = upstreams.sort((a, b) => (b.power || 0) - (a.power || 0)).slice(0, 5);
await Promise.all(top5.map((u) => fetchNeighboursForAsn(u.asn, currentDepth + 1)));
}
}
await fetchNeighboursForAsn(targetAsn, 0);
const edgeSet = new Set();
const uniqueEdges = edges.filter((e) => {
const key = e.from + "-" + e.to + "-" + e.relationship;
if (edgeSet.has(key)) return false;
edgeSet.add(key);
return true;
});
return { nodes: [...nodes.values()], edges: uniqueEdges, target_asn: targetAsn, depth: maxDepth };
}
module.exports = { fetchTopology };