Backend refactor reconciliation + ASPA verification fix + silent-failure audit #2

Merged
rene.fichtmueller merged 12 commits from merge-prod-snapshot into main 2026-07-16 20:18:49 +00:00
11 changed files with 138 additions and 23 deletions
Showing only changes of commit 141911537d - Show all commits

1
.gitignore vendored
View File

@ -49,6 +49,7 @@ audit/__pycache__/
audit/asn_registry.json
audit/latest_report.txt
backups/
.rollback/
public/index.html.bak
public/lia.html
server.js.bak*

View File

@ -1,5 +1,10 @@
#!/bin/bash
# PeerCortex safe deploy script — always backup before restart
# NOTE: this only covers server.js + public/index.html (the main site,
# PM2 process "peercortex"). The Fastify API server (PM2 process
# "peercortex-api", dist/api/index.js, port 3102) needs `npm run build`
# run first and `pm2 restart peercortex-api` separately -- see
# ecosystem.config.js on Erik for both app definitions.
BACKUP_DIR=/opt/peercortex-app/backups
mkdir -p $BACKUP_DIR
TS=$(date +%Y%m%d_%H%M%S)

View File

@ -3103,6 +3103,40 @@ async function fetchWhois(resource) {
return result;
}
// ============================================================
// Internal API Server Proxy
// ============================================================
// Several routes were extracted into src/api/ + src/features/*/routes.ts
// (Fastify, dist/api/index.js, run as a separate PM2 process on
// API_SERVER_PORT) but were never re-wired here after that migration —
// they just silently 404'd. This forwards those specific paths through
// rather than reimplementing them inline a second time.
const API_SERVER_PORT = parseInt(process.env.API_SERVER_PORT || "3102", 10);
function proxyToApiServer(req, res, targetUrl) {
const proxyReq = http.request(
{
hostname: "127.0.0.1",
port: API_SERVER_PORT,
path: targetUrl,
method: req.method,
headers: Object.assign({}, req.headers, { host: "127.0.0.1:" + API_SERVER_PORT }),
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on("error", (err) => {
console.error("[API Proxy] Error forwarding " + targetUrl + ":", err.message);
if (!res.headersSent) {
res.writeHead(502, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "API server unavailable", detail: err.message }));
}
});
req.pipe(proxyReq);
}
// ============================================================
// HTTP Server
// ============================================================
@ -5877,10 +5911,14 @@ const server = http.createServer(async (req, res) => {
// Feature 28: Submarine Cable overlay (TeleGeography proxy)
// ── Submarine Cables map data ─────────────────────────────────
// Migrated to src/features/submarine-cables/
if (reqPath === '/api/submarine-cables') {
return proxyToApiServer(req, res, req.url);
}
// ── Global datacenter/IXP map (PeeringDB proxy) ───────────────
// Migrated to src/features/global-infra/
if (reqPath === '/api/global-infra') {
return proxyToApiServer(req, res, req.url);
}
// ── Changelog page ─────────────────────────────────────────
@ -5927,26 +5965,39 @@ ${html}
}
// ── BGP Community Decoder ────────────────────────────────────
// Migrated to src/features/bgp-communities/
if (reqPath === '/api/communities') {
return proxyToApiServer(req, res, req.url);
}
// ── IRR Audit ─────────────────────────────────────────────────
// Migrated to src/features/irr-audit/
if (reqPath === '/api/irr-audit') {
return proxyToApiServer(req, res, req.url);
}
// ── AS-SET Expander ───────────────────────────────────────────
// Migrated to src/features/asset-expand/
if (reqPath === '/api/asset-expand') {
return proxyToApiServer(req, res, req.url);
}
// ── Routing History (prefix table via RIPE Stat routing-history) ──
// Migrated to src/features/rpki-history/
if (reqPath === '/api/rpki-history') {
return proxyToApiServer(req, res, req.url);
}
// ── AS-PATH Visualizer (RIPE Stat looking-glass) ────────────────
// Migrated to src/features/aspath/
if (reqPath === '/api/aspath') {
return proxyToApiServer(req, res, req.url);
}
// ── Looking Glass (RIPE Stat) ─────────────────────────────────
// Migrated to src/features/looking-glass/
if (reqPath === '/api/looking-glass') {
return proxyToApiServer(req, res, req.url);
}
// ── IXP Peering Matrix ────────────────────────────────────────
// Migrated to src/features/ix-matrix/
if (reqPath === '/api/ix-matrix') {
return proxyToApiServer(req, res, req.url);
}
// ── CORS preflight for all /api/webhooks + /api/hijacks ──────
@ -6137,16 +6188,26 @@ ${html}
// ── Hijack Alerts (legacy read) ───────────────────────────────
// Migrated to src/routes/hijack-alerts (Fastify feature)
// src/routes/hijack-alerts.ts registers /api/webhooks + /api/hijacks under
// the same paths this file already serves above (file-backed, live).
// Deliberately NOT proxied here — that would shadow working data with an
// empty Postgres-backed implementation. Not a gap, just two feature
// branches that ended up naming their routes the same thing.
// ── Changelog JSON API ────────────────────────────────────────
// Migrated to src/features/changelog/
if (reqPath === '/changelog-data') {
return proxyToApiServer(req, res, req.url);
}
// ── bio-rd RIB routes ─────────────────────────────────────────
// Migrated to src/features/rib/
if (reqPath.startsWith('/api/rib/')) {
return proxyToApiServer(req, res, req.url);
}
// ── Prefix Changes ──────────────────────────────────────────────
// Migrated to src/features/prefix-changes/
if (reqPath === '/api/prefix-changes') {
return proxyToApiServer(req, res, req.url);
}
// ============================================================
// FEATURE 2: PDF Export

49
src/api/index.ts Normal file
View File

@ -0,0 +1,49 @@
import fs from 'fs'
import { initializeDatabase, closeDatabaseConnection } from '../lib/db'
function loadEnv(): void {
const envPath = process.env.ENV_PATH ?? '/opt/peercortex-app/.env'
try {
const envContent = fs.readFileSync(envPath, 'utf8')
for (const line of envContent.split('\n')) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) continue
const eqIdx = trimmed.indexOf('=')
if (eqIdx > 0) {
const key = trimmed.substring(0, eqIdx).trim()
const val = trimmed.substring(eqIdx + 1).trim()
if (!process.env[key]) process.env[key] = val
}
}
console.log('[API] Environment variables loaded from', envPath)
} catch {
console.warn('[API] Could not read .env file at', envPath)
}
}
async function main(): Promise<void> {
loadEnv()
// initializeDatabase() must run before anything imports src/routes/hijack-alerts,
// which calls getDatabase() at module load time. Dynamic import defers evaluation
// of ./server (and its own static imports) until after the pool exists.
initializeDatabase()
const { startApiServer } = await import('./server.js')
const port = parseInt(process.env.API_PORT ?? '3102', 10)
await startApiServer(port)
}
async function shutdown(signal: string): Promise<void> {
console.log(`[API] ${signal} received, shutting down`)
await closeDatabaseConnection()
process.exit(0)
}
process.on('SIGTERM', () => void shutdown('SIGTERM'))
process.on('SIGINT', () => void shutdown('SIGINT'))
main().catch((error) => {
console.error('[API] Fatal startup error:', error)
process.exit(1)
})

View File

@ -17,7 +17,7 @@ import { changelogRoutes } from '../features/changelog/routes'
import { ribRoutes } from '../features/rib/routes'
import { hijackSubscribeRoutes } from '../features/hijack-subscribe/routes'
export async function createApiServer(port: number = 3100) {
export async function createApiServer(_port: number = 3102) {
const fastify = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? 'info',
@ -66,7 +66,7 @@ export async function createApiServer(port: number = 3100) {
return fastify
}
export async function startApiServer(port: number = 3100): Promise<void> {
export async function startApiServer(port: number = 3102): Promise<void> {
const server = await createApiServer(port)
try {

View File

@ -41,7 +41,6 @@ export class ASPAAdoptionScheduler {
stop(): void {
if (this.task) {
this.task.stop()
this.task.destroy()
this.task = null
console.log('[ASPA Scheduler] Job stopped')
}

View File

@ -29,7 +29,7 @@ export async function bgpCommunitiesRoutes(fastify: FastifyInstance): Promise<vo
throw new Error(`RIPE Stat API returned ${response.status}`);
}
const data = await response.json();
const data = await response.json() as any;
const rawComms: string[] = [];
if (data && data.data && data.data.bgp_state) {

View File

@ -12,7 +12,7 @@ interface CacheEntry {
export class PDFCacheManager {
private cache: Map<string, CacheEntry> = new Map()
private readonly TTL_MS = 5 * 60 * 1000
private cleanupInterval: NodeJS.Timer | null = null
private cleanupInterval: NodeJS.Timeout | null = null
constructor() {
this.startCleanup()

View File

@ -24,12 +24,12 @@ export class PDFRenderer {
const page = await this.browser.newPage()
try {
page.setDefaultTimeout(timeout_ms)
await page.setContent(html, { waitUntil: 'networkidle' })
const pdfBuffer = await page.pdf({
format: 'A4',
margin: { top: '0', right: '0', bottom: '0', left: '0' },
timeout: timeout_ms,
})
return pdfBuffer

View File

@ -29,7 +29,7 @@ export async function rpkiHistoryRoutes(fastify: FastifyInstance): Promise<void>
if (!response.ok) {
throw new Error(`RIPE Stat returned HTTP ${response.status}`);
}
const data = await response.json();
const data = await response.json() as any;
const byOrigin = (data && data.data && data.data.by_origin) || [];
const prefixes: any[] = [];

View File

@ -25,7 +25,7 @@ interface CreateHijackRequest {
export async function hijackAlertsRoutes(fastify: FastifyInstance): Promise<void> {
// Register webhook subscription
fastify.post<{ Querystring: { asn: string } }>(
fastify.post<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>(
'/webhooks',
async (request: FastifyRequest<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>, reply: FastifyReply) => {
try {