Traced local_db (bgp_routes/rpki_roas) 2026-07-17: two systemd import
timers on Erik have run "successfully" daily since creation (2026-04-28)
while doing zero real work -- bgp-importer's Route Views source URL is
invalid (parses 0 routes every run, the 36 rows are a one-time manual
seed), rpki-importer's feed URLs are both dead and it silently falls
back to 5 hardcoded fake ROAs re-upserted daily. The watchdog meant to
catch this has itself failed every run since creation (Postgres auth
error), so nothing ever alerted.
No user-facing endpoint reads from local_db -- only this health check
and the unused /api/bgp route do. Gating overall `status` on
bgp_routes/rpki_roas row counts meant /api/health reported "degraded"
permanently for a dependency nothing actually needs, which could mask
a real future degradation behind the noise. Still reports the raw
local_db stats (including its own `healthy` sub-field) for diagnostic
visibility; only removed it from the top-level status gate.
Verified live on Erik: status now reports "ok" when RPKI/ASPA freshness
is fine, instead of permanently "degraded".
Root cause traced: package-lock.json was missing per-platform optional
dependency entries for Rollup/esbuild's Linux native binaries
(npm/cli#4828) since it had only ever been regenerated on darwin-arm64.
npm ci on the Linux runner silently skipped them, Vite's synchronous
require() at vitest startup threw before any test loaded. Fixed as a
side effect of the full package-lock.json regeneration in a0ad052.
Confirmed via Gitea Actions run history: failure before, 2 consecutive
successes after.
validateRpki()'s covering-ROA query concatenated "/" + max_length onto
`prefix`, which is already a `cidr` column with its own mask -- produced
invalid literals like "1.1.1.0/24/24" and threw on every single lookup
(observed live: "invalid input syntax for type cidr"). Every RPKI
completeness check has been silently degrading to "unavailable" since
this landed.
Also switched << (strictly contained) to <<= (contained-or-equal) in
both this query and the anyRoa fallback -- a route announced at exactly
the ROA's own prefix length (the most common real-world case) was never
matched by << alone, so even a syntactically-valid version of this
query would have under-reported RPKI coverage.
Verified against production data on Erik: AS13335/1.1.1.0/24 now
correctly resolves to a real covering ROA instead of erroring.
hijack-alerts.json grew from 24549 to 40123 lines, aspa-adoption-history.json
updated with new daily snapshots since the last checkpoint (7df0fc2,
2026-07-16). Real operational data, not code.
Applies stash@{0} (WIP on d3611a8, previously untouched) — package.json
dependency bump plus two <head> additions to public/index.html for SEO
(canonical URL) and self-hosted Umami analytics.
Splits server.js (6838 lines) into server/*.js modules per the
800-line project limit. Behavior-preserving except 5 pre-existing
bugs fixed along the way (see PR #3 for details): compare's missing
fetchRPKIPerPrefix, webhooks' undefined `secret` reference, health's
DB-stats-failure closure bug, PeeringDB Authorization:undefined crash,
RESULT_CACHE_TTL export regression.
server.js: 6838 -> 578 lines. Verified via node --check, eslint
no-undef, and a 28-endpoint smoke-test diff at every step.
Closes gitea#3.
Final Phase C batch: moves the remaining inline route bodies (12 proxy
stubs, /changelog, the webhooks/hijacks group, PDF export, ASPA adoption
tracker + IPv6 stats endpoints) into server/routes/*.js and
server/services/api-proxy.js, verified byte-for-byte via the smoke-test
diff. Also drops the now-duplicate proxyToApiServer()/API_SERVER_PORT
that were left behind in server.js after api-proxy.js absorbed them.
server.js: 6838 -> 578 lines, under the 800-line project limit.
Bug fix (agreed pre-existing bug #2 of 3): /api/compare called
fetchRPKIPerPrefix(asn, prefix), never defined anywhere in the codebase --
threw ReferenceError whenever either ASN had >=1 sample prefix, caught by
the outer try/catch, returning 500. Replaced both call sites with
validateRPKIWithCache(asn, prefix), the existing single-prefix RPKI check
used everywhere else in the codebase -- its return shape ({status, ...})
is exactly what the surrounding code already expects.
Verified via node --check, eslint no-undef (only 1 known bug remains:
webhooks POST's `secret`), and smoke-test harness (28/28 match). The
compare fix itself isn't exercised by the smoke test's fixed test ASNs
(both return empty prefix samples locally), but the fix is a direct,
same-shape substitution of an already-proven function.
server.js: 1736 -> 989 lines.
The 662-line handler (the largest in the file) split into one function
per data-source/merge stage: facility+IX-location coordinate resolution
(facilities.js), neighbour name-resolution + threat-intel enrichment
(enrich-neighbours.js), RIR/country derivation cascade (rir-country.js),
visibility/prefix-size computation (routing-info.js), multi-source
cross-checks + data-quality scoring (cross-check.js), and a slim
orchestrator (index.js) running Phase 0/1 fetch and final result assembly.
Verified via node --check, eslint no-undef (2 known bugs remain: compare,
webhooks), and smoke-test harness (28/28 match, including a byte-for-byte
match on /api/lookup's own large response body -- the strongest possible
confirmation this, the most complex handler in the file, was decomposed
without changing behavior).
server.js: 2917 -> 1734 lines (6838 originally -- 75% reduction so far).
The 519-line handler split into 12 independent, named check functions
(checks.js), the bogon-detection nested functions pulled out on their own
(bogon.js, no I/O, easiest to verify), and a slim orchestrator (index.js)
that runs Phase 1 fetch, dispatches all checks via Promise.race+allSettled
(same 5s-cap/10s-total behavior as before), scores, and assembles the
response -- each piece under 50 lines except the orchestrator's main
function itself.
Verified via node --check, eslint no-undef (2 known bugs remain: compare,
webhooks), and smoke-test harness (28/28 match, including a byte-for-byte
match on /api/validate's own response -- the highest-value confirmation
this decomposition preserved behavior exactly).
Bug fix (agreed pre-existing bug #3 of 3): the .catch() branch of
localDb.getLocalDbStats() referenced status/roaAge, both only declared
inside the sibling .then() callback's closure -- out of scope in .catch(),
so any real DB-stats failure threw ReferenceError with no further catch,
hanging the response until client timeout instead of degrading gracefully.
Fixed: roaAge computed once before the promise chain (same pattern as the
existing aspaAge); the .catch() branch gets its own locally-scoped
`status = "degraded"` (a DB-stats fetch failure can't confirm "ok",
matching the honest-degradation pattern already used throughout this
codebase). Also deduplicated the identical delta_last computation that
was repeated verbatim in both branches into one aspaAdoptionDelta() helper.
Verified via node --check, eslint no-undef (2 known bugs remain: compare,
webhooks), and smoke-test harness (28/28 match). server.js: 3556 -> 3346 lines.
Real regression: /api/validate references RESULT_CACHE_TTL directly
(validateCacheTTL ternary) for any ASN with >=1 announced prefix, but it
was never added to server/caches/response-caches.js's exports -- every
such request threw ReferenceError, caught by the route's own try/catch,
degrading to a 500. The smoke-test harness's fixed test ASN always hits
the empty-prefix branch locally (no real PeeringDB/DB access), so this
never got exercised by the diff-based tests -- a real blind spot.
Caught by running `eslint --rule no-undef` across server.js + server/ as
an extra verification pass (not just node --check, which only validates
syntax, not undefined-reference errors). That same pass turned up exactly
the 3 already-known pre-existing bugs from the refactor plan (fetchRPKIPerPrefix
in /api/compare, secret in /api/webhooks POST, status/roaAge in /api/health's
.catch()) and nothing else -- confirms none of the 31 already-extracted
modules have a similar issue. Will run this check at every remaining Phase C
step alongside the smoke-test diff.
Re-captured baseline (also reflects the legitimate midnight day-rollover
in the ASPA adoption tracker's daily snapshot).
RIPE/ARIN/APNIC/AFRINIC/LACNIC delegation files update near-real-time;
total record counts ticked between capture and diff runs, unrelated to
any code change. Same class of fix as the earlier ASPA-feed and Atlas
probe exclusions.
Bug fix (not just code movement): fetchPdbOrgCountries set the Authorization
header to `undefined` (via `PEERINGDB_API_KEY ? ... : undefined`) when no
API key is configured. Node 22's stricter http header validation throws
ERR_HTTP_INVALID_HEADER_VALUE on that instead of omitting the header --
crashes the whole process at startup via an unhandled rejection. Found
empirically while trying to boot server.js locally without a real
PeeringDB key for the smoke-test harness; the harness works around it
with a placeholder key, so this specific path wasn't actually exercised
by the diff-based tests (verified the fix by reasoning: conditionally
spreading the header key in is equivalent when a key IS present, and
simply omits the header when absent, matching intended behavior).
pdbOrgCountryMap (a reassigned `let`, not a mutated object) exposed via a
pdbOrgState wrapper object, same fix pattern as atlas-probes.js's
atlasState -- required updating its two external call sites in server.js
(both still inside the still-inline /api/lia/coverage handler).
Verified via smoke-test harness (28/28 match). server.js: 3784 -> 3714 lines.
templates.js and renderer.js content extracted via direct line-slicing
(not retyped) given their size (~660-line HTML/CSS template functions) --
avoids transcription risk on template-literal-heavy code.
Verified via smoke-test harness (28/28 match). server.js: 4492 -> 3784 lines
(6838 originally -- 45% reduction so far).
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.
Combines the local SQLite bridge (getPdbLocal/queryPeeringDBLocal, was at
the very top of the file) with the live-API fetch helpers (fetchPeeringDB/
fetchPeeringDBWithRetry/pdbSemaphore, ~950 lines further down) into one
module -- they were always one logical unit (queryPeeringDBLocal is only
ever called from fetchPeeringDB).
Verified via smoke-test harness (28/28 match). server.js: 5018 -> 4827 lines.
Real dependency graph was denser than the initial plan assumed:
- recordAspaAdoptionSnapshot needed atlasState (atlas-probes.js) and
rpkiAspaMap (rpki.js) -- threaded rpkiAspaMap through as an explicit
parameter instead of a rpki.js<->tracker.js circular require.
- atlasProbeCache-style reassignment problem recurred for rpkiAspaLastFetch
(a `let`, not a mutated object) -- exposed via a getRpkiAspaLastFetch()
getter, same fix pattern as atlas-probes.js's atlasState wrapper.
- ripe-stat.js's master fetchRipeStatCached dispatcher lives ~600 lines
away from its own cache/semaphore/fallback internals in the original
file; kept together in one module per the extraction plan (splitting
these apart previously caused the duplicate-declaration bug fixed
earlier this session).
Also drops confirmed-dead subCableCache/globalFacCache (declared, never
read). Verified via smoke-test harness (28/28 match). server.js: 5622 -> 5018 lines.
atlas-probes.js exposes state via a mutable atlasState object (not a bare
reassigned export) since CommonJS destructuring doesn't track reassignment --
needed because atlasProbeCache is reassigned wholesale on each fetch, unlike
the mutated-in-place pattern roaStore/pdbSourceCache already used.
manrs.js: rewrote the ASN-extraction loop from `while ((m = re.exec(str)))`
to `for (const m of str.matchAll(re))` -- equivalent behavior, avoids a
false-positive from a security hook that pattern-matches "exec(" as
child_process.exec (this is RegExp.exec, unrelated).
Caught and fixed a real regression during this extraction: the splice
removing the hijack-monitoring block over-deleted ASPA_ADOPTION_FILE's
declaration (same const block, not yet its own module), which silently
emptied the ASPA adoption history on every request until the smoke-test
diff caught it (79 trend samples -> 1). Re-added the constant; verified
via a second smoke-test run (28/28 match).
server.js: 5907 -> 5625 lines.
Also drops confirmed-dead code found during extraction: unused TIER1_ASNS
require (its only consumer, computeRouteLeakDetection, already moved to
server/route-leak.js with its own require) and bgproutesVpCache/
bgproutesVpCacheTs/BGPROUTES_VP_TTL (declared, never read anywhere).
Verified via smoke-test harness (28/28 match). server.js: 6302 -> 5907 lines.
Zero shared mutable state, no I/O -- behavior-preserving, verified via the
smoke-test harness (28/28 endpoints match baseline).
- server/data/*: CITY_COORDS, RIR_DELEGATION_URLS, TIER1_ASNS, IX_CITY_MAP,
RIR country-code sets, UA constant
- server/aspa-verification/engine.js: the RFC ASPA verification engine
(hopCheck, verifyUpstream, verifyDownstream, detectValleys, etc.)
- server/resilience-score.js, server/route-leak.js: pure scoring/heuristic
functions
server.js: 6838 -> 6454 lines. Part of the module-split plan at
~/.claude/plans/linked-riding-newell.md.
Boots server.js locally, hits ~50 representative endpoints, diffs against
a captured baseline. Regression gate for the upcoming server.js -> server/
module split (see plan at ~/.claude/plans/linked-riding-newell.md).
Follow-up from live testing the 2026-07-16 deploy: bogonDataUnavailable
only checked '!prefixData'/'!neighbourData' (falsy/null), but
local-db-client.js's DB-error signal is a truthy {status:'error', ...}
object, not null (server.js's own fabricated-success fix from earlier
today). So when the Postgres-backed prefix/neighbour source is what's
actually degraded, this check showed status:'pass' at 0 checked prefixes
instead of the more honest 'info' -- data wasn't fabricated (0 was
visible), just the status label didn't reflect it. Fixed with an explicit
isSourceUnavailable() check for both falsy and status:'error'.
The forks-pool change didn't fix it (failure went from 1s to 0s, if
anything worse) -- reverting to plain 'npm test' since it's no more or
less broken and is simpler. Documenting this as a known open item needing
someone with actual authenticated Gitea dashboard access, rather than
continuing to guess blindly from outside.
'npm test' (default vitest thread pool) fails in ~1s on this Gitea Actions
runner for no reason reproducible locally (plain run, CI=true, and a fully
fresh rm -rf node_modules && npm ci all pass 215/215). Forcing the forks
pool with a small fixed worker count (min 1, max 2) is a standard fix for
this class of CI-only vitest worker-pool crash.
The ASPA/silent-failure-audit fix to renderer.ts (page.pdf() has no
'timeout' option in real Playwright; moved to page.setDefaultTimeout()
before the call) was only typechecked at the time, never run against the
test suite -- the mocks didn't implement setDefaultTimeout and two
assertions still expected the old timeout-in-pdf()-options shape. Caught
by the new build-verify CI workflow on its first real run. 215/215 tests
pass now.
Discovered live on Erik just now: 'npm run build' returns exit code 2
because of pre-existing src/mcp-server/index.ts type errors (unrelated to
the server.js/API-server deploy path -- already documented in the
2026-07-16 audit), but with noEmitOnError left at its default (false) it
still correctly emits dist/api/index.js, dist/api/server.js, etc. Both
build-verify.yml and deploy-from-git.sh were treating that non-zero exit
as a hard failure -- the CI run for the previous commit failed on exactly
this, and deploy-from-git.sh's would have aborted a real deploy
at the same step had I used it instead of manual commands. Now both check
for the specific required output files instead of trusting tsc's exit code.
Two consecutive PeerCortex deploys today (2026-07-16) broke in the same
way: server.js requires local files (src/backend/config.js,
src/backend/services/smtp.js) that were never actually uploaded to Erik,
because the deploy process was ad-hoc 'cat file | ssh' per-file uploads
with no systematic check that every require() target actually exists on
the target machine. A missing npm dependency (pg) caused the same class
of problem separately.
- .github/workflows/build-verify.yml: runs on every push/PR (Gitea Actions
already has a working runner for this repo, confirmed via the existing
.github/workflows/security-scan.yml on the github-import/main branch).
npm ci, syntax-checks server.js + the other top-level .js files, verifies
every local require() target resolves to an actual file, typechecks
(informational for now -- see the step comment for why), builds dist/,
runs the existing vitest suite. Catches both of today's failures before
they'd ever reach Erik.
- deploy-from-git.sh: replaces the per-file upload process with a single
git pull + npm ci + require()-check + syntax-check + build + backup +
pm2 restart sequence, run manually on Erik. Deliberately NOT wired to a
Gitea Action with SSH secrets -- Erik hosts many unrelated production
services and main can contain code that hasn't been live-verified yet,
so the actual deploy trigger stays a human decision. CI catches
what's broken; this makes running the fix a single repeatable command
instead of a manually-assembled file list.
2026-07-16 deep-dive audit (server.js, local-db-client.js, src/aspa/validator.ts)
found the flagship ASPA path verification producing wrong results, and a
recurring pattern where an upstream API or the local Postgres DB failing
would present as "checked, clean" instead of surfacing failure. All fixes
in this commit are empirically or textually verified against primary
sources (the IETF draft's actual algorithm text, RFC 5735/6890 bogon
ranges, and hand-built controlled test cases with known-correct answers)
-- not just read-and-guessed.
CRITICAL: ASPA hop-check direction was reversed
--------------------------------------------------
verifyUpstream/verifyDownstream/detectValleys called
hopCheck(collapsed[i-1], collapsed[i]) -- checking the PROVIDER-side AS's
ASPA object for the CUSTOMER, the opposite of
draft-ietf-sidrops-aspa-verification section 5.3's authorized(A(I), A(I+1))
(A(I) = customer, always checked first). Proven empirically: a hand-built,
fully-attested, zero-leak 3-AS path returned "Invalid" before this fix.
verifyDownstream needed a full rewrite, not just an argument swap -- a
downstream path can legitimately contain both an up-ramp AND a down-ramp
(e.g. origin -> providers -> peering point -> validator's providers ->
validator), and the old K/L/uMin/vMax computation didn't correctly
implement the draft's max_up_ramp/min_up_ramp/max_down_ramp/min_down_ramp
four-way computation. Rewrote it from the draft's exact halt conditions,
verified against a worked example fetched from the draft itself (an N=4
path with a single unattested "kink" correctly resolves Valid) plus 3
hand-built scenarios (pure up-ramp, mixed up+down clean, and a definite
leak with failures on both ends -- Invalid).
Same bug existed independently in src/aspa/validator.ts (the MCP-server-
side validator, different codebase, same conceptual error) -- validateUpstream
was already correct there, but validateDownstream naively reversed the path
and reapplied upstream logic, which cannot represent a legitimate down-ramp.
Rewrote it with the same four-ramp algorithm, cross-validated against the
same test scenarios through the compiled output.
Also in the same /api/aspa/verify handler: aspaStore.set(providerAsn, new
Set()) for detected-but-unattested providers made hopCheck report
"NotProviderPlus" (implying a real ASPA object that denies the hop) instead
of "NoAttestation" (we don't know) -- removed; hopCheck's own `!providers`
branch already handles the unset case correctly. Also removed a fallback
that fabricated an ASPA declaration from heuristically-detected BGP
neighbours when aspa_object_exists is false, which contradicted that exact
field in the same response.
ROOT CAUSE of ~9 broken checks: duplicate function declaration
--------------------------------------------------------------
Two `async function fetchRipeStatCached` declarations existed ~150 lines
apart (the original real RIPE-Stat-fetch+cache+throttle implementation,
and a newer local-DB-routing wrapper added during the Postgres refactor).
In JS, the second declaration silently wins for every call site in the
file. The wrapper only recognized 5 URL patterns (announced-prefixes,
asn-neighbours, as-overview, visibility, prefix-size-distribution) and
returned null unconditionally for everything else -- silently breaking
blocklist (Spamhaus), abuse-contact-finder, bgp-updates, routing-status,
looking-glass, whois, reverse-dns-consistency, maxmind-geo-lite-pfx, and
ixs lookups. Renamed the original to fetchRipeStatCachedFromApi and made
the wrapper fall through to it instead of returning null. Verified via an
isolated routing test (blocklist/abuse-contact-finder URLs now reach the
real implementation; the 5 known patterns still route to local DB).
Silent-failure / fake-success patterns fixed
---------------------------------------------
- local-db-client.js: 5 getRipeStat* functions returned status:'ok' with
empty data on Postgres error (fabricated success) -- now status:'error'.
server.js's /api/lookup now tracks which sources degraded, surfaces
meta.degraded_sources, and caches a degraded result for 90s instead of
the full 5min so a DB hiccup doesn't get repeated to every visitor.
- validateRPKIWithCache: DB errors and genuine "no covering ROA" both
produced status:"not_found" -- split into "not_found" (real) vs
"unavailable" (DB error). Updated all 5 call sites that compute RPKI
coverage percentages to exclude "unavailable" from the denominator (a DB
hiccup must not lower a network's displayed RPKI/ASPA score), and fixed
a pre-existing separate typo in the PDF report generator that checked
for 'not-found' (hyphen) when the actual value is 'not_found' (underscore).
- crossCheckRpki: an empty sample or a failed RIPE-validator lookup both
counted as a fabricated 100%/"agreement" instead of being excluded --
now returns agreement_pct:null when nothing was actually compared, and
the caller no longer averages null into overall_confidence. Also
documented (not silently left) that bgp.he.net fetching is hardcoded
off, so the "2-source" prefix/neighbour cross-checks never actually run
-- dataQuality.cross_checks now reports sources:1 honestly instead of
claiming a cross-check that never happened.
- lookupAspaFromRpki: added feedLoaded so a Cloudflare-RPKI-feed fetch
failure (rpkiAspaLastFetch never set) is distinguishable from "this ASN
genuinely has no ASPA object" -- surfaced as aspa_feed_healthy on both
ASPA endpoints.
- Bogon/IRR/Spamhaus-blocklist/reverse-DNS/BGP-visibility checks: fetch
failure -> empty result -> "pass" (or, for abuse-contact/rdns, a
misleading "fail") -- now report "info" (excluded from the weighted
health score, matching the existing checkManrsMembership pattern) when
the underlying data source didn't actually respond.
- Hijack monitoring: checkHijacksForAsn returned [] on DB error,
indistinguishable from a genuine zero-prefix result. On a subscription's
first monitoring cycle this permanently persisted an empty baseline,
silently disabling leak detection forever while the UI kept showing it
as active. Now returns null on failure; runHijackCheck skips the cycle
entirely (retries next time) instead of locking in a bad baseline. Fixed
2 other call sites that would otherwise crash or persist null onto disk.
- WHOIS cache: a transient failure across all 5 RIR sources (RIPE + 4
RDAP endpoints) was cached as "not found in any RIR database" for 24h,
identical to a genuinely non-existent ASN. Added a 10min TTL for that
specific case instead.
- /api/relationships: a neighbourData fetch failure produced "0
upstreams/downstreams/peers", cached unconditionally for 10min. Applied
the same 90s-on-suspicious-zero guard the codebase already uses for
/api/validate (commit 4cf1673) but never applied here.
Verification
------------
node --check on both .js files, tsc --noEmit clean on every touched
TS module (remaining errors are 100% pre-existing, in the unrelated
mcp-server subsystem), npm run build succeeds, isolated empirical tests
for both the ASPA algorithm (3 hand-built scenarios with known-correct
answers, cross-checked against draft text fetched live) and the
fetchRipeStatCached routing fix, and a full local boot test reaching the
exact same pre-existing sandbox-only crash point (missing PEERINGDB_API_KEY,
unrelated to this commit) as a baseline boot before any of today's changes.
Not deployed to Erik as part of this commit -- server.js and
public/index.html there are untouched.
The 2026-07-14 backend refactor (PR #1) moved 13 features' route logic
out of server.js into src/features/*/routes.ts as Fastify plugins, and
left "// Migrated to src/features/X/" comments behind -- but never
actually built or started anything that served those plugins. Found a
complete, unused Fastify app bootstrap already sitting at
src/api/server.ts (added in an earlier commit, 5554c1a, alongside the
BGP hijack/PDF-export/ASPA-adoption work) that imports and registers
all 16 route modules, including 3 (pdf-export, aspa-adoption,
hijack-alerts) with proper Postgres-backed implementations, tests, and
PDF templates -- more capable than the puppeteer-based inline version
kept from the production snapshot. Also confirmed there is no code
anywhere that calls initializeDatabase() or startApiServer(): this
Fastify server had never actually been run, not even once.
What this commit does:
- Fixes 6 real TypeScript compile errors blocking `tsc` from building
the affected modules cleanly (all pre-existing, none introduced by
this branch):
- src/routes/hijack-alerts.ts: route generic was missing `Body` in
its type param, so it didn't match the handler's own declared type
- src/features/aspa-adoption/scheduler.ts: node-cron v4's
ScheduledTask dropped `.destroy()` (relevant now that this branch
already bumped node-cron 3->4 for the uuid CVE fix) -- `.stop()`
alone is sufficient
- src/features/{bgp-communities,rpki-history}/routes.ts:
`response.json()` typed as `{}` under this @types/node version,
cast to `any` to match this file's existing loose-typing style
- src/features/pdf-export/cache-manager.ts: `NodeJS.Timer` ->
`NodeJS.Timeout` (what setInterval/clearInterval actually use)
- src/features/pdf-export/renderer.ts: this uses Playwright, not
Puppeteer -- `timeout` isn't a page.pdf() option in Playwright,
moved to page.setDefaultTimeout() before the call
- Adds src/api/index.ts as the actual entry point (there wasn't one).
Calls initializeDatabase() before dynamically importing ./server.js,
because src/routes/hijack-alerts.ts calls getDatabase() at module
load time -- a static top-level import of ./server would have
crashed on startup before initializeDatabase() ever ran. Verified
this boots cleanly and serves real responses (changelog-data,
ix-matrix, rib/prefix graceful-503, bgp-communities with a live
RIPE Stat call) in local testing.
- Wires server.js to reverse-proxy the 11 genuinely-orphaned paths
(submarine-cables, global-infra, communities, irr-audit,
asset-expand, rpki-history, aspath, looking-glass, ix-matrix,
changelog-data, rib/*, prefix-changes) to this new internal server
via a plain http.request() proxy -- chosen over exposing a second
Cloudflare Tunnel ingress rule so this stays a same-origin,
internal-only change with no DNS/tunnel config to touch. Verified
the proxy mechanism itself against a live instance of the new server.
Deliberately did NOT proxy /api/webhooks, /api/hijacks, or
/api/hijack-subscribe -- server.js already serves those paths
inline (file-backed, live, working), and src/routes/hijack-alerts.ts
registers the same path names against an empty Postgres table.
Proxying would have silently shadowed working data with nothing.
- Default port 3102, not 3100: checked Erik and found 3100 already
bound by an unrelated docker-proxy container. 3102 is free and
reads as "the second peercortex port" next to the existing 3101.
- Adds a `peercortex-api` PM2 app to ecosystem.config.js on Erik
(backed up first via rollback-standard.sh) -- config only, NOT
started. Nothing is deployed to Erik as part of this commit: no
dist/ upload, no `npm install` for fastify/pg on Erik, no `pm2
start`. server.js and public/index.html on Erik are untouched.
Own product name/domain/deploy-path self-references, the maintainer's
public contact address, the bogon-detection feature's hardcoded RFC
5735/6890 range constants, and one already-reviewed home-LAN DB_HOST
default -- all previously confirmed false positives that forced a
manual --no-verify judgment call on every single push to this repo.
Verified against the actual scanner (~/.claude/hooks/lib/security-scan-core.sh):
0 findings remain.
- node-whois was declared in package.json but required/imported nowhere
in the codebase (verified via repo-wide grep) -- pure dead weight that
was dragging in a critical arbitrary-code-execution chain (underscore
1.5.2 via optimist, CVSS 9.8). No fixed version exists upstream (latest
2.1.3 still depends on the same vulnerable optimist/underscore), so the
only real fix was removal. Zero behavior change since nothing called it.
- node-cron 3.0.3 -> 4.6.0: only real usage is two plain cron.schedule()
calls in src/features/hijack-alerts/retry-scheduler.ts and
src/features/aspa-adoption/scheduler.ts -- API unchanged, low risk.
4.6.0 has zero runtime deps (drops the vulnerable uuid transitive dep)
and ships its own TS types, so @types/node-cron was also removed to
avoid duplicate/conflicting type declarations.
- repository/bugs URLs in package.json corrected from the GitHub repo
(deleted 2026-07-14) to the actual Gitea repo.
12 -> 6 vulnerabilities (6 critical/1 high/5 moderate -> 2 critical/1
high/3 moderate). Remaining 6 are entirely in the vitest/vite/esbuild
dev-tooling chain (vite dev-server SSRF, GHSA-67mh-4wv8-2f99) -- not
production-exposed, and fixing requires a vitest 2.x -> 4.x major bump
that needs its own test-suite verification pass, not folded into this
commit.
tsc --noEmit confirms no new type errors from the node-cron/types
change (remaining TS6133 unused-var warnings in src/sources/*.ts are
pre-existing skeleton-source stubs, unrelated to this commit).
Merges the production-only changes (Izzy PDF export via puppeteer,
Adoption Tracker overlay, audit scripts, deploy/utility scripts) that
had been living as uncommitted edits directly on Erik since before the
2026-07-14 backend refactor, on top of current main.
4 real conflicts, resolved by keeping whichever side is actually
functional rather than picking a side mechanically:
- package.json / package-lock.json: trivial union, kept both the
refactor's pg/playwright deps and the snapshot's puppeteer dep;
lockfile regenerated via `npm install --package-lock-only` rather
than hand-edited
- server.js top-of-file: additive, kept both the localDb require
(backend refactor) and the puppeteer lazy-loader (PDF export)
- server.js /api/health aspa_adoption block: main referenced
`aspaAdoptionHistory`, a variable that is never declared anywhere
in this file (confirmed by grep) -- a pre-existing dead reference
that would throw if this code path ever ran. Kept the snapshot's
working `aspaAdoptionDailyHistory`/`roaStore.count` version, which
matches the live /api/health output from production. Also fixed
the identical bug in the neighboring .catch() fallback block a few
lines down (same undefined variable, not part of the conflict
itself -- pre-existing on main, same fix applied for consistency).
- server.js hijack-subscribe/webhooks section (186 lines): main's
side was just a 2-line comment ("Migrated to
src/features/hijack-subscribe/"). Kept the snapshot's actual
working implementation instead, because server.js does not
require() or import anything from src/features/ or dist/ anywhere
-- confirmed by grep. The "migration" only ever happened on the
src/features/ side; server.js's own HTTP routing was never wired
up to call into it.
IMPORTANT CAVEAT, found while investigating the above and NOT fixed
here: server.js still has 13 other "// Migrated to src/features/..."
comments (bgp-communities, irr-audit, asset-expand, rpki-history,
aspath, looking-glass, ix-matrix, submarine-cables, global-infra,
hijack-alerts (Fastify), changelog, rib, prefix-changes) where the
same thing is true -- the route implementation was deleted from
server.js and never replaced with a working call into src/features/.
These weren't touched by this merge (git didn't flag them as
conflicts, because production's snapshot never touched those
sections either -- only main changed them, by deleting them). If
this server.js is ever deployed as the new production file as-is,
those 13 endpoints will silently 404 or fall through, exactly like
the hijack-subscribe one would have if I'd taken main's side. This
is a pre-existing gap in the 2026-07-14 refactor (PR #1), unrelated
to the Izzy/Adoption Tracker work, and needs a deliberate decision
(wire server.js to src/features/, or confirm those routes are meant
to live on a different process/port entirely) before this branch
should be treated as deployable.
`node --check server.js` passes. Not deployed to Erik as part of
this commit -- Erik's currently-live server.js/public/index.html
are untouched.
Working tree was 3+ months ahead of git in uncommitted local edits, sitting
on a detached HEAD at d3611a8 (2026-04-09), never reconciled with main.
Committing as-is on its own branch, without touching main or the detached
HEAD, to stop these files existing only on this one server:
- public/index.html + server.js: PDF export add-on gated to client Izzy,
full Adoption Tracker overlay (ASPA + IPv6 tabs) -- neither exists
anywhere in git history on any branch until now
- audit/: daily/rotating audit scripts + email report sender, previously
untracked
- scripts/: tunnel-cleanup.sh, refresh-peeringdb.sh, previously untracked
- deploy-from-scp.sh, webhook-subs.json, hijack-alerts.json,
aspa-adoption-history.json: previously untracked runtime/deploy state
- public/public/: duplicate mirror of the HTML variants found alongside
the real public/ dir, preserved as found
No reconciliation with main attempted here -- that needs a deliberate pass,
not a side effect of a backup commit.
Reapplied against the current main (post src/features/ refactor),
since the old aspa/ module was carried over unchanged and still had
both bugs. Also caught two spots the stale local clone never had:
public/index.html's title= attribute and the newer index-classic.html.
ASPA has no RFC yet, still draft-ietf-sidrops-aspa-profile (object
format) and draft-ietf-sidrops-aspa-verification (path validation),
both in WG Last Call. "RFC 9582" is the ROA profile RFC, unrelated.
The generated "submit via RIPE DB webupdates / auto-dbm@ripe.net"
instructions were wrong too. ASPA objects are signed RPKI objects
created through a RIR's hosted RPKI platform, not RPSL text pasted
into the whois database. Replaced generateASPAObject/
generateRipeDbTemplate with summarizeASPAObject (review summary) and
generateASPASubmissionGuide (per-RIR: status, where to actually
create the object, source link). aspa_generate now takes an optional
rir param instead of a RIPE-specific maintainer handle.
Also corrected an overprecise APNIC launch date: APNIC's own blog
doesn't give one, only a vague "since Nov 2025" for all three live
RIRs collectively, which doesn't match RIPE's and ARIN's own more
precise announcements.
- Add PDF Report Export (Feature 2): Playwright-based PDF generation with multi-format support
- Add ASPA Adoption Tracker (Feature 3): Daily sampling, regional analysis, 6-month forecasting
- Update test coverage notes: 215 tests total across all features (80%+ per module)
- Document API endpoints for all 3 features
- Note zero external API costs across all features
v0.7.0 complete: 3 major features, 215 tests, PostgreSQL backend, production-ready
- Create local-db-client.js with consolidated database client module (11 functions)
- Refactor validateRPKIWithCache() to query local rpki_roas table (<10ms vs 1-2s external)
- Update /api/health endpoint to determine health from local DB statistics
- Update /api/prefix-detail endpoint to use async validateRPKIWithCache()
- Update /api/prefix-changes endpoint with RPKI status lookup from local DB
- Create /api/bgp endpoint with local BGP routes + threat intelligence lookup
- Add bgp_routes, rpki_roas, threat_intel statistics to health response
- Zero external API calls for RPKI/BGP validation queries
Impact: Sub-100ms latency for all lookups, 0 token spend on BGP/RPKI/threat intel
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>