morphit/apps/indexer/scripts/rpc-endpoints-probe-smoke.ts

76 lines
3.1 KiB
TypeScript

/**
* rpc-endpoints-probe-smoke — cp453 (t.txt #1)
*
* The Settings "RPC endpoints" refresh button asks the indexer to ACTIVELY ping
* every canonical Blurt node for fresh latency. Two invariants matter:
*
* DDoS guard — a GLOBAL 5s cache means no volume of clicks can make the indexer
* ping the upstream nodes more than once per 5s (Ken: "we don't want some kid
* trying to ddos our server with massive clicks").
*
* PRIVACY #1 — the browser never touches a Blurt node; the INDEXER probes them,
* and only the canonical PUBLIC nodes (`DEFAULT_BLURT_RPC_ENDPOINTS`), never the
* operator's private upstreams. main.ts must wire the canonical list, not the
* raw config endpoints.
*/
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
probeEndpoints,
cachedProbeEndpoints,
__resetProbeCacheForTests
} from '../src/api/rpcHealth.ts';
let failures = 0;
function check(name: string, cond: boolean): void {
console.log(` ${cond ? '✓' : '✗'} ${name}`);
if (!cond) failures++;
}
// Invalid hosts: the probe fetches fail fast, so we test SHAPE + CACHE, not net.
const urls = ['https://node-a.invalid', 'https://node-b.invalid'];
const r = await probeEndpoints(urls);
check(
'probeEndpoints returns exactly the URLs it was given (never invents a host)',
r.endpoints.length === 2 && r.endpoints.map((e) => e.url).join(',') === urls.join(',')
);
check(
'a failed/unreachable probe is unhealthy + null latency, never throws',
r.endpoints.every((e) => e.healthy === false && e.latency_ms === null)
);
// 5s server-side cache (DDoS guard): two calls within 5s share ONE probe.
__resetProbeCacheForTests();
const a = await cachedProbeEndpoints(urls);
const b = await cachedProbeEndpoints(urls);
check('cachedProbeEndpoints serves the SAME cached object within 5s (no re-ping)', a === b);
__resetProbeCacheForTests();
const c = await cachedProbeEndpoints(urls);
check('after the cache is cleared, a fresh probe is produced', c !== a);
// PRIVACY: main.ts must wire the CANONICAL public list to the route, not the raw
// config endpoints (which may include the operator's private upstream URLs).
const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
const main = readFileSync(join(repo, 'apps/indexer/src/main.ts'), 'utf8');
check(
'PRIVACY: the rpc-endpoints route is wired with DEFAULT_BLURT_RPC_ENDPOINTS (canonical/public only)',
/rpcEndpointsRoute\([\s\S]*?DEFAULT_BLURT_RPC_ENDPOINTS\)/.test(main) &&
!/rpcEndpointsRoute\([\s\S]*?config\.blurtRpcEndpoints/.test(main)
);
// The route rate-limits probes AND keeps the passive default cheap.
const health = readFileSync(join(repo, 'apps/indexer/src/api/rpcHealth.ts'), 'utf8');
check(
'the probe is gated behind ?probe=1 (default stays the cheap passive snapshot)',
/query\('probe'\) === '1'/.test(health) && /PROBE_MIN_INTERVAL_MS = 5_000/.test(health)
);
if (failures === 0) {
console.log('✓ all 6 rpc-endpoints-probe scenarios passed');
} else {
console.log(`\n✗ ${failures}/6 rpc-endpoints-probe scenarios failed`);
process.exit(1);
}