const fs = require("fs"); const { DATA_DIR } = require("../hijack-monitoring/store"); const { atlasState } = require("../atlas-probes"); const ASPA_ADOPTION_FILE = DATA_DIR + '/aspa-adoption-history.json'; // Load persisted ASPA adoption history from disk. // Returns array of snapshot objects (up to 365 entries). function loadAspaAdoptionHistory() { try { const raw = JSON.parse(fs.readFileSync(ASPA_ADOPTION_FILE, 'utf8')); return Array.isArray(raw) ? raw : []; } catch(_) { return []; } } // Save ASPA adoption history to disk (keep last 365 entries). function saveAspaAdoptionHistory(history) { try { const trimmed = history.slice(-365); fs.writeFileSync(ASPA_ADOPTION_FILE, JSON.stringify(trimmed, null, 2), 'utf8'); } catch(e) { console.error('[ASPA-ADOPT] Save failed:', e.message); } } // In-memory adoption history (persisted on each new snapshot). Mutated // in place (push / index-assign), never reassigned -- safe to export as a // live array reference, same pattern as roaStore/pdbSourceCache. let aspaAdoptionDailyHistory = loadAspaAdoptionHistory(); // Take a new adoption snapshot and persist it. Called whenever the RPKI feed // is refreshed. // @param {number} aspaCount — total ASPA objects in rpkiAspaMap // @param {number} roaCount — total ROAs in roaStore // @param {Map} rpkiAspaMap — customer_asid -> Set, owned by // server/services/rpki.js; passed in explicitly (rather than required here) // to avoid a circular require between this module and rpki.js. function recordAspaAdoptionSnapshot(aspaCount, roaCount, rpkiAspaMap) { const now = new Date(); const date = now.toISOString().slice(0, 10); // "YYYY-MM-DD" // Compute how many Atlas-visible ASNs have ASPA let atlasTotal = 0; let atlasWithAspa = 0; if (atlasState.cache?.asns_with_probes) { atlasTotal = atlasState.cache.asns_with_probes.length; // rpkiAspaMap is keyed by integer ASN atlasWithAspa = atlasState.cache.asns_with_probes.filter(a => rpkiAspaMap.has(Number(a))).length; } const coveragePct = atlasTotal > 0 ? Math.round((atlasWithAspa / atlasTotal) * 10000) / 100 // 2 decimal places : 0; // Compute delta from previous snapshot const prev = aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1]; const delta = prev ? aspaCount - prev.aspa_objects : 0; const snapshot = { date, ts: now.getTime(), aspa_objects: aspaCount, roa_count: roaCount, atlas_asns_total: atlasTotal, atlas_asns_with_aspa: atlasWithAspa, coverage_pct_atlas: coveragePct, aspa_delta: delta, method: 'rpki_cloudflare_feed', }; // Only store one snapshot per date (overwrite if same day) const existingIdx = aspaAdoptionDailyHistory.findIndex(s => s.date === date); if (existingIdx >= 0) { aspaAdoptionDailyHistory[existingIdx] = snapshot; } else { aspaAdoptionDailyHistory.push(snapshot); } saveAspaAdoptionHistory(aspaAdoptionDailyHistory); console.log(`[ASPA-ADOPT] Snapshot ${date}: ${aspaCount} objects, ${coveragePct}% Atlas coverage (${atlasWithAspa}/${atlasTotal})`); return snapshot; } // Get adoption trend for a given period string ('7d', '30d', '90d', '1y'). function getAdoptionTrend(period) { const days = period === '7d' ? 7 : period === '30d' ? 30 : period === '90d' ? 90 : 365; const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; return aspaAdoptionDailyHistory.filter(s => s.ts >= cutoff); } // Build the ASPA adoption dashboard HTML. function buildAspaAdoptionDashboard() { const latest = aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1]; const trend30 = getAdoptionTrend('30d'); const trend7 = getAdoptionTrend('7d'); // Forecast: simple linear regression over last 30 days function linearForecast(data, daysAhead) { if (data.length < 2) return null; const n = data.length; const xs = data.map((_, i) => i); const ys = data.map(d => d.coverage_pct_atlas); const xMean = xs.reduce((a, b) => a + b, 0) / n; const yMean = ys.reduce((a, b) => a + b, 0) / n; const num = xs.reduce((s, x, i) => s + (x - xMean) * (ys[i] - yMean), 0); const den = xs.reduce((s, x) => s + (x - xMean) ** 2, 0); if (den === 0) return yMean; const slope = num / den; return Math.max(0, Math.min(100, yMean + slope * (n - 1 + daysAhead))); } const forecast90d = linearForecast(trend30, 90); const change7d = trend7.length >= 2 ? (trend7[trend7.length-1].aspa_objects - trend7[0].aspa_objects) : 0; const trendLabels = trend30.map(d => d.date); const trendValues = trend30.map(d => d.coverage_pct_atlas); const asnValues = trend30.map(d => d.aspa_objects); const coverageNow = latest?.coverage_pct_atlas ?? 0; const aspaNow = latest?.aspa_objects ?? 0; const atlasTotal = latest?.atlas_asns_total ?? 0; return ` ASPA Adoption Tracker · PeerCortex

ASPA Adoption Tracker

Global ASPA deployment trends · Powered by RPKI Cloudflare feed · Updated every ~4 hours

Atlas Coverage
${coverageNow.toFixed(1)}%
of Atlas-visible ASNs
ASPA Objects
${aspaNow.toLocaleString()}
total in RPKI
Atlas ASNs
${atlasTotal.toLocaleString()}
sampled denominator
7-Day Delta
${change7d > 0 ? '+' : ''}${change7d}
new ASPA objects
Data Points
${aspaAdoptionDailyHistory.length}
history snapshots

Atlas Coverage % (last 30 days)

90-Day Forecast
${forecast90d !== null ? forecast90d.toFixed(1) + '%' : '—'}
linear projection from 30-day trend
Based on ${trend30.length} data points.
Current: ${coverageNow.toFixed(1)}%

Total ASPA Objects in RPKI (last 30 days)

Last snapshot: ${latest?.date ?? 'none'} · Denominator: ${atlasTotal.toLocaleString()} Atlas ASNs · JSON API
`; } module.exports = { aspaAdoptionDailyHistory, loadAspaAdoptionHistory, saveAspaAdoptionHistory, recordAspaAdoptionSnapshot, getAdoptionTrend, buildAspaAdoptionDashboard, };