feat(bio-rd): add local RIB integration via bio-rd gRPC client
Adds real-time local BGP RIB data as a complement to external APIs (RIPE Stat, bgproutes.io) which have rate limits and 15min+ delays. - bio-rd-client.js: gRPC client for bio-routing/bio-rd RIS service - LPM, Get, GetLonger, GetRouters, DumpRIB, ObserveRIB methods - IPv4/IPv6 encoding as uint64 pair (bio.net format) - Full BGP path decode: AS paths, communities, large communities - Graceful fallback if RIS unavailable (null/empty returns) - protos/: bio-rd proto definitions (ris, bgp, session, route, net) - server.js: three new endpoints + WebSocket stream - GET /api/rib/prefix — LPM + more-specifics via GetLonger - GET /api/rib/routers — list BMP-monitored routers - GET /api/rib/dump — full RIB dump with ASN filter + limit - WS /ws/rib — live ObserveRIB stream (add/withdraw events) - package.json: @grpc/grpc-js + @grpc/proto-loader dependencies
This commit is contained in:
parent
f1fe96132f
commit
344ee15338
448
bio-rd-client.js
Normal file
448
bio-rd-client.js
Normal file
@ -0,0 +1,448 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* bio-rd gRPC client wrapper for PeerCortex.
|
||||
*
|
||||
* Wraps the RoutingInformationService (RIS) from bio-rd.
|
||||
* proto package: bio.ris (cmd/ris/api/ris.proto)
|
||||
* proto package: bio.net (net/api/net.proto)
|
||||
* proto package: bio.route (route/api/route.proto)
|
||||
*
|
||||
* Usage:
|
||||
* const { createRisClient } = require('./bio-rd-client');
|
||||
* const ris = createRisClient('localhost', 4321);
|
||||
* const routes = await ris.dumpRib('router1', '0:0', 0);
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const grpc = require('@grpc/grpc-js');
|
||||
const protoLoader = require('@grpc/proto-loader');
|
||||
|
||||
const PROTO_DIR = path.join(__dirname, 'protos');
|
||||
const RIS_PROTO = path.join(PROTO_DIR, 'cmd/ris/api/ris.proto');
|
||||
|
||||
const LOADER_OPTIONS = {
|
||||
keepCase: true,
|
||||
longs: String, // uint64 fields come back as strings (avoids JS BigInt issues)
|
||||
enums: String, // enum values come back as their string names
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
includeDirs: [PROTO_DIR],
|
||||
};
|
||||
|
||||
const HIDDEN_REASON_MAP = {
|
||||
HiddenReasonNone: null,
|
||||
HiddenReasonNextHopUnreachable: 'next-hop-unreachable',
|
||||
HiddenReasonFilteredByPolicy: 'filtered-by-policy',
|
||||
HiddenReasonASLoop: 'as-loop',
|
||||
HiddenReasonOurOriginatorID: 'our-originator-id',
|
||||
HiddenReasonClusterLoop: 'cluster-loop',
|
||||
HiddenReasonOTCMismatch: 'otc-mismatch',
|
||||
};
|
||||
|
||||
const ORIGIN_MAP = { 0: 'IGP', 1: 'EGP', 2: 'Incomplete' };
|
||||
|
||||
// ─── IP helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Convert a dotted-decimal or colon-hex IP string to the bio.net.IP proto object.
|
||||
* uint64 fields are passed as strings (matching longs:'String' loader option).
|
||||
*/
|
||||
function ipToProto(ipString) {
|
||||
if (ipString.includes(':')) {
|
||||
return _ipv6ToProto(ipString);
|
||||
}
|
||||
return _ipv4ToProto(ipString);
|
||||
}
|
||||
|
||||
function _ipv4ToProto(ipString) {
|
||||
const parts = ipString.split('.').map(Number);
|
||||
if (parts.length !== 4 || parts.some(isNaN)) {
|
||||
throw new Error(`Invalid IPv4 address: ${ipString}`);
|
||||
}
|
||||
const val = ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
|
||||
return { higher: '0', lower: String(val), version: 'IPv4' };
|
||||
}
|
||||
|
||||
function _ipv6ToProto(ipString) {
|
||||
// Expand :: notation and parse into two uint64 halves
|
||||
const expanded = _expandIPv6(ipString);
|
||||
const groups = expanded.split(':').map(g => parseInt(g, 16));
|
||||
const higher = BigInt(groups[0]) << 48n
|
||||
| BigInt(groups[1]) << 32n
|
||||
| BigInt(groups[2]) << 16n
|
||||
| BigInt(groups[3]);
|
||||
const lower = BigInt(groups[4]) << 48n
|
||||
| BigInt(groups[5]) << 32n
|
||||
| BigInt(groups[6]) << 16n
|
||||
| BigInt(groups[7]);
|
||||
return { higher: String(higher), lower: String(lower), version: 'IPv6' };
|
||||
}
|
||||
|
||||
function _expandIPv6(addr) {
|
||||
if (addr.includes('::')) {
|
||||
const [left, right] = addr.split('::');
|
||||
const leftGroups = left ? left.split(':') : [];
|
||||
const rightGroups = right ? right.split(':') : [];
|
||||
const missing = 8 - leftGroups.length - rightGroups.length;
|
||||
const mid = Array(missing).fill('0');
|
||||
return [...leftGroups, ...mid, ...rightGroups].join(':');
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a bio.net.IP proto object back to a human-readable IP string.
|
||||
*/
|
||||
function protoToIp(protoIp) {
|
||||
if (!protoIp) return null;
|
||||
const version = protoIp.version || 'IPv4';
|
||||
const lower = BigInt(protoIp.lower || '0');
|
||||
const higher = BigInt(protoIp.higher || '0');
|
||||
|
||||
if (version === 'IPv4' || version === 0) {
|
||||
const n = Number(lower);
|
||||
return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.');
|
||||
}
|
||||
|
||||
// IPv6: reassemble 8 groups of 16 bits from two uint64 values
|
||||
const mask16 = 0xffffn;
|
||||
const groups = [
|
||||
(higher >> 48n) & mask16,
|
||||
(higher >> 32n) & mask16,
|
||||
(higher >> 16n) & mask16,
|
||||
higher & mask16,
|
||||
(lower >> 48n) & mask16,
|
||||
(lower >> 32n) & mask16,
|
||||
(lower >> 16n) & mask16,
|
||||
lower & mask16,
|
||||
];
|
||||
return groups.map(g => g.toString(16)).join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a CIDR string to a bio.net.Prefix proto object.
|
||||
*/
|
||||
function prefixToProto(cidr) {
|
||||
const [ipStr, lenStr] = cidr.split('/');
|
||||
const length = lenStr !== undefined ? parseInt(lenStr, 10) : (ipStr.includes(':') ? 128 : 32);
|
||||
return { address: ipToProto(ipStr), length };
|
||||
}
|
||||
|
||||
// ─── Route decoding ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Decode a bio.route.Route proto object to a clean JS object.
|
||||
* Handles both DumpRIBReply.route and direct Route messages.
|
||||
*/
|
||||
function decodeRoute(risRoute) {
|
||||
if (!risRoute) return null;
|
||||
|
||||
// DumpRIBReply wraps the route in a .route field
|
||||
const route = risRoute.route || risRoute;
|
||||
if (!route || !route.pfx) return null;
|
||||
|
||||
const pfxAddr = protoToIp(route.pfx.address);
|
||||
const pfxLen = route.pfx.length || 0;
|
||||
const prefix = `${pfxAddr}/${pfxLen}`;
|
||||
|
||||
const paths = (route.paths || []).map(_decodePath);
|
||||
|
||||
// Flatten: return first path's attributes at top level, paths array for multi-path
|
||||
const first = paths[0] || {};
|
||||
return {
|
||||
prefix,
|
||||
nextHop: first.nextHop || null,
|
||||
asPaths: first.asPaths || [],
|
||||
communities: first.communities || [],
|
||||
largeCommunities: first.largeCommunities || [],
|
||||
localPref: first.localPref || 0,
|
||||
med: first.med || 0,
|
||||
origin: first.origin || null,
|
||||
ebgp: first.ebgp || false,
|
||||
hidden: first.hidden || false,
|
||||
hiddenReason: first.hiddenReason || null,
|
||||
paths,
|
||||
};
|
||||
}
|
||||
|
||||
function _decodePath(path) {
|
||||
const hiddenReasonStr = typeof path.hidden_reason === 'string'
|
||||
? path.hidden_reason
|
||||
: Object.keys(HIDDEN_REASON_MAP)[path.hidden_reason] || 'HiddenReasonNone';
|
||||
const hidden = hiddenReasonStr !== 'HiddenReasonNone';
|
||||
const hiddenReason = HIDDEN_REASON_MAP[hiddenReasonStr] || null;
|
||||
|
||||
if (path.type === 'BGP' || path.type === 1) {
|
||||
return _decodeBgpPath(path.bgp_path, hidden, hiddenReason);
|
||||
}
|
||||
if (path.type === 'Static' || path.type === 0) {
|
||||
return _decodeStaticPath(path.static_path, hidden, hiddenReason);
|
||||
}
|
||||
|
||||
return { nextHop: null, asPaths: [], communities: [], largeCommunities: [],
|
||||
localPref: 0, med: 0, origin: null, ebgp: false, hidden, hiddenReason };
|
||||
}
|
||||
|
||||
function _decodeBgpPath(bgp, hidden, hiddenReason) {
|
||||
if (!bgp) return { nextHop: null, asPaths: [], communities: [], largeCommunities: [],
|
||||
localPref: 0, med: 0, origin: null, ebgp: false, hidden, hiddenReason };
|
||||
|
||||
const asPaths = (bgp.as_path || []).flatMap(seg => seg.asns || []).map(Number);
|
||||
|
||||
const communities = (bgp.communities || []).map(c => {
|
||||
const n = Number(c);
|
||||
return `${(n >> 16) & 0xffff}:${n & 0xffff}`;
|
||||
});
|
||||
|
||||
const largeCommunities = (bgp.large_communities || []).map(lc =>
|
||||
`${lc.global_administrator}:${lc.data_part1}:${lc.data_part2}`
|
||||
);
|
||||
|
||||
return {
|
||||
nextHop: protoToIp(bgp.next_hop),
|
||||
asPaths,
|
||||
communities,
|
||||
largeCommunities,
|
||||
localPref: Number(bgp.local_pref || 0),
|
||||
med: Number(bgp.med || 0),
|
||||
origin: ORIGIN_MAP[bgp.origin] || 'Unknown',
|
||||
ebgp: bgp.ebgp || false,
|
||||
hidden,
|
||||
hiddenReason,
|
||||
};
|
||||
}
|
||||
|
||||
function _decodeStaticPath(staticPath, hidden, hiddenReason) {
|
||||
return {
|
||||
nextHop: staticPath ? protoToIp(staticPath.next_hop) : null,
|
||||
asPaths: [], communities: [], largeCommunities: [],
|
||||
localPref: 0, med: 0, origin: 'Static', ebgp: false,
|
||||
hidden, hiddenReason,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Client factory ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load the RIS proto definition (cached after first call).
|
||||
*/
|
||||
let _packageDef = null;
|
||||
function _loadPackageDef() {
|
||||
if (_packageDef) return _packageDef;
|
||||
_packageDef = protoLoader.loadSync(RIS_PROTO, LOADER_OPTIONS);
|
||||
return _packageDef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RIS gRPC client.
|
||||
* Returns null if the package definition cannot be loaded.
|
||||
*/
|
||||
function createRisClient(host = 'localhost', port = 4321) {
|
||||
let RisService;
|
||||
try {
|
||||
const packageDef = _loadPackageDef();
|
||||
const proto = grpc.loadPackageDefinition(packageDef);
|
||||
RisService = proto.bio.ris.RoutingInformationService;
|
||||
} catch (err) {
|
||||
console.error('[bio-rd] Failed to load proto definition:', err.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const address = `${host}:${port}`;
|
||||
const channelOptions = {
|
||||
'grpc.connect_timeout_ms': 5000,
|
||||
'grpc.initial_reconnect_backoff_ms': 1000,
|
||||
};
|
||||
|
||||
const channel = new grpc.Channel(
|
||||
address,
|
||||
grpc.credentials.createInsecure(),
|
||||
channelOptions
|
||||
);
|
||||
|
||||
const client = new RisService(address, grpc.credentials.createInsecure(), channelOptions);
|
||||
|
||||
// ── Unary helper ────────────────────────────────────────────────────────────
|
||||
function _unary(method, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = new Date(Date.now() + 5000);
|
||||
client[method](request, { deadline }, (err, response) => {
|
||||
if (err) return reject(err);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Stream collector ────────────────────────────────────────────────────────
|
||||
function _collectStream(method, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const results = [];
|
||||
const deadline = new Date(Date.now() + 30000);
|
||||
const call = client[method](request, { deadline });
|
||||
call.on('data', chunk => results.push(chunk));
|
||||
call.on('error', reject);
|
||||
call.on('end', () => resolve(results));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Longest Prefix Match for a given prefix.
|
||||
* Returns the best matching route or null.
|
||||
*/
|
||||
async function lpm(router, prefix) {
|
||||
try {
|
||||
const response = await _unary('lPM', {
|
||||
router,
|
||||
vrf: '0:0',
|
||||
pfx: prefixToProto(prefix),
|
||||
});
|
||||
const routes = (response.routes || []).map(decodeRoute).filter(Boolean);
|
||||
return routes[0] || null;
|
||||
} catch (err) {
|
||||
console.warn(`[bio-rd] lpm(${router}, ${prefix}) failed:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact prefix lookup.
|
||||
* Returns the matching route or null.
|
||||
*/
|
||||
async function get(router, prefix) {
|
||||
try {
|
||||
const response = await _unary('get', {
|
||||
router,
|
||||
vrf: '0:0',
|
||||
pfx: prefixToProto(prefix),
|
||||
});
|
||||
const routes = (response.routes || []).map(decodeRoute).filter(Boolean);
|
||||
return routes[0] || null;
|
||||
} catch (err) {
|
||||
console.warn(`[bio-rd] get(${router}, ${prefix}) failed:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all more-specific prefixes covered by the given prefix.
|
||||
* Returns an array of decoded routes.
|
||||
*/
|
||||
async function getLonger(router, prefix) {
|
||||
try {
|
||||
const response = await _unary('getLonger', {
|
||||
router,
|
||||
vrf: '0:0',
|
||||
pfx: prefixToProto(prefix),
|
||||
});
|
||||
return (response.routes || []).map(decodeRoute).filter(Boolean);
|
||||
} catch (err) {
|
||||
console.warn(`[bio-rd] getLonger(${router}, ${prefix}) failed:`, err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all routers known to this RIS instance.
|
||||
* Returns an array of { name, address } objects.
|
||||
*/
|
||||
async function getRouters() {
|
||||
try {
|
||||
const response = await _unary('getRouters', {});
|
||||
return (response.routers || []).map(r => ({
|
||||
name: r.sys_name || '',
|
||||
address: r.address || '',
|
||||
}));
|
||||
} catch (err) {
|
||||
console.warn('[bio-rd] getRouters() failed:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the full RIB for a router/VRF, optionally filtered by origin ASN.
|
||||
* Collects the entire stream and returns an array of decoded routes.
|
||||
*
|
||||
* @param {string} router - Router identifier (e.g. "router1")
|
||||
* @param {string} vrfName - VRF name (e.g. "0:0" for default)
|
||||
* @param {number} originAsn - Filter by originating AS (0 = no filter)
|
||||
*/
|
||||
async function dumpRib(router, vrfName = '0:0', originAsn = 0) {
|
||||
try {
|
||||
const request = {
|
||||
router,
|
||||
vrf: vrfName,
|
||||
afisafi: 'IPv4Unicast',
|
||||
filter: { originating_asn: originAsn || 0 },
|
||||
};
|
||||
const chunks = await _collectStream('dumpRIB', request);
|
||||
return chunks.map(decodeRoute).filter(Boolean);
|
||||
} catch (err) {
|
||||
console.warn(`[bio-rd] dumpRib(${router}) failed:`, err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe real-time RIB updates for a router/VRF.
|
||||
*
|
||||
* @param {string} router - Router identifier
|
||||
* @param {string} vrfName - VRF name (e.g. "0:0")
|
||||
* @param {function} onUpdate - Called with { type: 'add'|'withdraw', route }
|
||||
* @param {function} onError - Called with the error on stream failure
|
||||
* @returns {function} cancel - Call to stop the observation
|
||||
*/
|
||||
function observeRib(router, vrfName = '0:0', onUpdate, onError) {
|
||||
const request = {
|
||||
router,
|
||||
vrf: vrfName,
|
||||
afisafi: 'IPv4Unicast',
|
||||
};
|
||||
|
||||
let call;
|
||||
try {
|
||||
call = client.observeRIB(request);
|
||||
} catch (err) {
|
||||
if (onError) onError(err);
|
||||
return () => {};
|
||||
}
|
||||
|
||||
call.on('data', (update) => {
|
||||
if (!update || !update.route) return;
|
||||
const type = update.advertisement ? 'add' : 'withdraw';
|
||||
const route = decodeRoute(update.route);
|
||||
if (route && onUpdate) onUpdate({ type, route });
|
||||
});
|
||||
|
||||
call.on('error', (err) => {
|
||||
console.warn(`[bio-rd] observeRib(${router}) stream error:`, err.message);
|
||||
if (onError) onError(err);
|
||||
});
|
||||
|
||||
call.on('end', () => {
|
||||
console.log(`[bio-rd] observeRib(${router}) stream ended`);
|
||||
});
|
||||
|
||||
return () => {
|
||||
try { call.cancel(); } catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Cleanup ─────────────────────────────────────────────────────────────────
|
||||
function close() {
|
||||
try { channel.close(); } catch (_) {}
|
||||
try { client.close(); } catch (_) {}
|
||||
}
|
||||
|
||||
return { lpm, get, getLonger, getRouters, dumpRib, observeRib, close };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRisClient,
|
||||
ipToProto,
|
||||
prefixToProto,
|
||||
protoToIp,
|
||||
decodeRoute,
|
||||
};
|
||||
329
package-lock.json
generated
329
package-lock.json
generated
@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "peercortex",
|
||||
"version": "0.1.0",
|
||||
"version": "0.6.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "peercortex",
|
||||
"version": "0.1.0",
|
||||
"version": "0.6.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cheerio": "^1.0.0",
|
||||
@ -774,6 +776,37 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
|
||||
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
||||
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.5.3",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
|
||||
@ -905,6 +938,16 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-sdsl/ordered-map": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz",
|
||||
@ -956,6 +999,70 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz",
|
||||
@ -1373,7 +1480,6 @@
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@ -1844,7 +1950,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@ -2167,11 +2272,82 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@ -2184,7 +2360,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
@ -2580,6 +2755,15 @@
|
||||
"@esbuild/win32-x64": "0.27.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@ -3161,6 +3345,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@ -3524,7 +3717,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -3713,6 +3905,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@ -3720,6 +3918,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||
@ -4315,6 +4519,30 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -4450,6 +4678,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@ -5176,7 +5413,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
@ -5972,6 +6208,83 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@ -63,6 +63,8 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cheerio": "^1.0.0",
|
||||
@ -73,7 +75,6 @@
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/node-whois": "^2.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitest/coverage-v8": "^2.1.0",
|
||||
|
||||
104
protos/cmd/ris/api/ris.proto
Normal file
104
protos/cmd/ris/api/ris.proto
Normal file
@ -0,0 +1,104 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package bio.ris;
|
||||
|
||||
import "net/api/net.proto";
|
||||
import "route/api/route.proto";
|
||||
option go_package = "github.com/bio-routing/bio-rd/cmd/ris/api";
|
||||
|
||||
service RoutingInformationService {
|
||||
rpc LPM(LPMRequest) returns (LPMResponse) {};
|
||||
rpc Get(GetRequest) returns (GetResponse) {};
|
||||
rpc GetRouters(GetRoutersRequest) returns (GetRoutersResponse) {};
|
||||
rpc GetLonger(GetLongerRequest) returns (GetLongerResponse) {};
|
||||
rpc ObserveRIB(ObserveRIBRequest) returns (stream RIBUpdate);
|
||||
rpc DumpRIB(DumpRIBRequest) returns (stream DumpRIBReply);
|
||||
}
|
||||
|
||||
message LPMRequest {
|
||||
string router = 1;
|
||||
uint64 vrf_id = 2;
|
||||
string vrf = 4;
|
||||
bio.net.Prefix pfx = 3;
|
||||
}
|
||||
|
||||
message LPMResponse {
|
||||
repeated bio.route.Route routes = 1;
|
||||
}
|
||||
|
||||
message GetRequest {
|
||||
string router = 1;
|
||||
uint64 vrf_id = 2;
|
||||
string vrf = 4;
|
||||
bio.net.Prefix pfx = 3;
|
||||
}
|
||||
|
||||
message GetResponse {
|
||||
repeated bio.route.Route routes = 1;
|
||||
}
|
||||
|
||||
message GetLongerRequest {
|
||||
string router = 1;
|
||||
uint64 vrf_id = 2;
|
||||
string vrf = 4;
|
||||
bio.net.Prefix pfx = 3;
|
||||
}
|
||||
|
||||
message GetLongerResponse {
|
||||
repeated bio.route.Route routes = 1;
|
||||
}
|
||||
|
||||
message ObserveRIBRequest {
|
||||
string router = 1;
|
||||
uint64 vrf_id = 2;
|
||||
string vrf = 4;
|
||||
enum AFISAFI {
|
||||
IPv4Unicast = 0;
|
||||
IPv6Unicast = 1;
|
||||
}
|
||||
AFISAFI afisafi = 3;
|
||||
bool allow_unready_rib = 5;
|
||||
}
|
||||
|
||||
message RIBFilter {
|
||||
uint32 originating_asn = 1;
|
||||
uint32 min_length = 2;
|
||||
uint32 max_length = 3;
|
||||
}
|
||||
|
||||
message RIBUpdate {
|
||||
bool advertisement = 1;
|
||||
bool is_initial_dump = 3;
|
||||
bool end_of_rib = 4;
|
||||
bio.route.Route route = 2;
|
||||
}
|
||||
|
||||
message DumpRIBRequest {
|
||||
string router = 1;
|
||||
uint64 vrf_id = 2;
|
||||
string vrf = 4;
|
||||
enum AFISAFI {
|
||||
IPv4Unicast = 0;
|
||||
IPv6Unicast = 1;
|
||||
}
|
||||
AFISAFI afisafi = 3;
|
||||
RIBFilter filter = 5;
|
||||
}
|
||||
|
||||
message DumpRIBReply {
|
||||
bio.route.Route route = 1;
|
||||
}
|
||||
|
||||
message GetRoutersRequest {
|
||||
|
||||
}
|
||||
|
||||
message Router {
|
||||
string sys_name = 1;
|
||||
repeated uint64 vrf_ids = 2;
|
||||
string address = 3;
|
||||
}
|
||||
|
||||
message GetRoutersResponse {
|
||||
repeated Router routers = 1;
|
||||
}
|
||||
19
protos/net/api/net.proto
Normal file
19
protos/net/api/net.proto
Normal file
@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package bio.net;
|
||||
option go_package = "github.com/bio-routing/bio-rd/net/api";
|
||||
|
||||
message Prefix {
|
||||
IP address = 1;
|
||||
uint32 length = 2;
|
||||
}
|
||||
|
||||
message IP {
|
||||
uint64 higher = 1;
|
||||
uint64 lower = 2;
|
||||
enum Version {
|
||||
IPv4 = 0;
|
||||
IPv6 = 1;
|
||||
}
|
||||
Version version = 3;
|
||||
}
|
||||
34
protos/protocols/bgp/api/bgp.proto
Normal file
34
protos/protocols/bgp/api/bgp.proto
Normal file
@ -0,0 +1,34 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package bio.bgp;
|
||||
|
||||
import "net/api/net.proto";
|
||||
import "route/api/route.proto";
|
||||
import "protocols/bgp/api/session.proto";
|
||||
option go_package = "github.com/bio-routing/bio-rd/protocols/bgp/api";
|
||||
|
||||
message ListSessionsRequest {
|
||||
SessionFilter filter = 1;
|
||||
}
|
||||
|
||||
message SessionFilter {
|
||||
bio.net.IP neighbor_ip = 1;
|
||||
string vrf_name = 2;
|
||||
}
|
||||
|
||||
message ListSessionsResponse {
|
||||
repeated Session sessions = 1;
|
||||
}
|
||||
|
||||
message DumpRIBRequest {
|
||||
bio.net.IP peer = 1;
|
||||
uint32 afi = 2;
|
||||
uint32 safi = 3;
|
||||
string vrf_name = 4;
|
||||
}
|
||||
|
||||
service BgpService {
|
||||
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse) {}
|
||||
rpc DumpRIBIn(DumpRIBRequest) returns (stream bio.route.Route) {}
|
||||
rpc DumpRIBOut(DumpRIBRequest) returns (stream bio.route.Route) {}
|
||||
}
|
||||
35
protos/protocols/bgp/api/session.proto
Normal file
35
protos/protocols/bgp/api/session.proto
Normal file
@ -0,0 +1,35 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package bio.bgp;
|
||||
|
||||
import "net/api/net.proto";
|
||||
option go_package = "github.com/bio-routing/bio-rd/protocols/bgp/api";
|
||||
|
||||
message Session {
|
||||
bio.net.IP local_address = 1;
|
||||
bio.net.IP neighbor_address = 2;
|
||||
uint32 local_asn = 3;
|
||||
uint32 peer_asn = 4;
|
||||
enum State {
|
||||
Disabled = 0;
|
||||
Idle = 1;
|
||||
Connect = 2;
|
||||
Active = 3;
|
||||
OpenSent = 4;
|
||||
OpenConfirmed = 5;
|
||||
Established = 6;
|
||||
}
|
||||
State status = 5;
|
||||
SessionStats stats = 6;
|
||||
uint64 established_since = 7;
|
||||
string description = 8;
|
||||
}
|
||||
|
||||
message SessionStats {
|
||||
uint64 messages_in = 1;
|
||||
uint64 messages_out = 2;
|
||||
uint64 flaps = 3;
|
||||
uint64 routes_received = 4;
|
||||
uint64 routes_imported = 5;
|
||||
uint64 routes_exported = 6;
|
||||
}
|
||||
80
protos/route/api/route.proto
Normal file
80
protos/route/api/route.proto
Normal file
@ -0,0 +1,80 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package bio.route;
|
||||
|
||||
import "net/api/net.proto";
|
||||
option go_package = "github.com/bio-routing/bio-rd/route/api";
|
||||
|
||||
message Route {
|
||||
bio.net.Prefix pfx = 1;
|
||||
repeated Path paths = 2;
|
||||
}
|
||||
|
||||
message Path {
|
||||
enum Type {
|
||||
Static = 0;
|
||||
BGP = 1;
|
||||
}
|
||||
enum HiddenReason {
|
||||
HiddenReasonNone = 0;
|
||||
HiddenReasonNextHopUnreachable = 1;
|
||||
HiddenReasonFilteredByPolicy = 2;
|
||||
HiddenReasonASLoop = 3;
|
||||
HiddenReasonOurOriginatorID = 4;
|
||||
HiddenReasonClusterLoop = 5;
|
||||
HiddenReasonOTCMismatch = 6;
|
||||
}
|
||||
Type type = 1;
|
||||
StaticPath static_path = 2;
|
||||
BGPPath bgp_path = 3;
|
||||
HiddenReason hidden_reason = 4;
|
||||
uint32 time_learned = 5;
|
||||
GRPPath grp_path = 6;
|
||||
}
|
||||
|
||||
message StaticPath {
|
||||
bio.net.IP next_hop = 1;
|
||||
}
|
||||
|
||||
message GRPPath {
|
||||
bio.net.IP next_hop = 1;
|
||||
map<string,string> meta_data = 2;
|
||||
}
|
||||
|
||||
message BGPPath {
|
||||
uint32 path_identifier = 1;
|
||||
bio.net.IP next_hop = 2;
|
||||
uint32 local_pref = 3;
|
||||
repeated ASPathSegment as_path = 4;
|
||||
uint32 origin = 5;
|
||||
uint32 med = 6;
|
||||
bool ebgp = 7;
|
||||
uint32 bgp_identifier = 8;
|
||||
bio.net.IP source = 9;
|
||||
repeated uint32 communities = 10;
|
||||
repeated LargeCommunity large_communities = 11;
|
||||
uint32 originator_id = 12;
|
||||
repeated uint32 cluster_list = 13;
|
||||
repeated UnknownPathAttribute unknown_attributes = 14;
|
||||
bool bmp_post_policy = 15;
|
||||
uint32 only_to_customer = 16;
|
||||
}
|
||||
|
||||
message ASPathSegment {
|
||||
bool as_sequence = 1;
|
||||
repeated uint32 asns = 2;
|
||||
}
|
||||
|
||||
message LargeCommunity {
|
||||
uint32 global_administrator = 1;
|
||||
uint32 data_part1 = 2;
|
||||
uint32 data_part2 = 3;
|
||||
}
|
||||
|
||||
message UnknownPathAttribute {
|
||||
bool optional = 1;
|
||||
bool transitive = 2;
|
||||
bool partial = 3;
|
||||
uint32 type_code = 4;
|
||||
bytes value = 5;
|
||||
}
|
||||
129
server.js
129
server.js
@ -24,6 +24,18 @@ try {
|
||||
const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || "";
|
||||
const BGPROUTES_API_URL = process.env.BGPROUTES_API_URL || "https://api.bgproutes.io/v1";
|
||||
|
||||
// bio-rd gRPC client (optional — graceful fallback if unavailable)
|
||||
let risClient = null;
|
||||
try {
|
||||
const { createRisClient } = require('./bio-rd-client');
|
||||
const BIO_RD_HOST = process.env.BIO_RD_HOST || 'localhost';
|
||||
const BIO_RD_PORT = parseInt(process.env.BIO_RD_PORT || '4321');
|
||||
risClient = createRisClient(BIO_RD_HOST, BIO_RD_PORT);
|
||||
console.log(`[bio-rd] RIS client configured → ${BIO_RD_HOST}:${BIO_RD_PORT}`);
|
||||
} catch (e) {
|
||||
console.log('[bio-rd] RIS client not available (bio-rd-client.js missing or gRPC not installed)');
|
||||
}
|
||||
|
||||
const PEERINGDB_API_KEY = process.env.PEERINGDB_API_KEY || "";
|
||||
const PEERINGDB_API_URL = process.env.PEERINGDB_API_URL || "https://www.peeringdb.com/api";
|
||||
|
||||
@ -4899,6 +4911,81 @@ ${html}
|
||||
}
|
||||
}
|
||||
|
||||
// ── bio-rd RIB: prefix lookup ──────────────────────────────────
|
||||
if (reqPath === '/api/rib/prefix') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
if (!risClient) { res.writeHead(503); return res.end(JSON.stringify({ error: 'bio-rd RIS not configured' })); }
|
||||
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||
const prefix = params.get('prefix') || '';
|
||||
const routerParam = params.get('router') || 'default';
|
||||
if (!prefix) { res.writeHead(400); return res.end(JSON.stringify({ error: 'prefix required' })); }
|
||||
try {
|
||||
const t0 = Date.now();
|
||||
const routers = await risClient.getRouters();
|
||||
const routerName = (routerParam === 'default' && routers.length > 0) ? routers[0] : routerParam;
|
||||
const [routes, longer] = await Promise.all([
|
||||
risClient.lpm(routerName, prefix),
|
||||
risClient.getLonger(routerName, prefix),
|
||||
]);
|
||||
res.writeHead(200);
|
||||
return res.end(JSON.stringify({
|
||||
prefix,
|
||||
router: routerName,
|
||||
routes: routes || [],
|
||||
moreSpecifics: (longer || []).slice(0, 20),
|
||||
source: 'bio-rd-local',
|
||||
latencyMs: Date.now() - t0,
|
||||
}));
|
||||
} catch(e) {
|
||||
res.writeHead(500); return res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
// ── bio-rd RIB: list routers ───────────────────────────────────
|
||||
if (reqPath === '/api/rib/routers') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
if (!risClient) { res.writeHead(503); return res.end(JSON.stringify({ error: 'bio-rd RIS not configured' })); }
|
||||
try {
|
||||
const routers = await risClient.getRouters();
|
||||
res.writeHead(200);
|
||||
return res.end(JSON.stringify({ routers: routers || [], source: 'bio-rd-local' }));
|
||||
} catch(e) {
|
||||
res.writeHead(500); return res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
// ── bio-rd RIB: dump ───────────────────────────────────────────
|
||||
if (reqPath === '/api/rib/dump') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
if (!risClient) { res.writeHead(503); return res.end(JSON.stringify({ error: 'bio-rd RIS not configured' })); }
|
||||
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||
const router = params.get('router') || '';
|
||||
const asnFilter = params.get('asn') ? parseInt(params.get('asn')) : undefined;
|
||||
const limit = Math.min(parseInt(params.get('limit') || '100', 10), 1000);
|
||||
if (!router) { res.writeHead(400); return res.end(JSON.stringify({ error: 'router required' })); }
|
||||
try {
|
||||
const t0 = Date.now();
|
||||
const allRoutes = await risClient.dumpRib(router, 'default', asnFilter);
|
||||
const routes = (allRoutes || []).slice(0, limit);
|
||||
res.writeHead(200);
|
||||
return res.end(JSON.stringify({
|
||||
router,
|
||||
routes,
|
||||
total: (allRoutes || []).length,
|
||||
source: 'bio-rd-local',
|
||||
latencyMs: Date.now() - t0,
|
||||
}));
|
||||
} catch(e) {
|
||||
res.writeHead(500); return res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
res.writeHead(404);
|
||||
res.end(
|
||||
@ -5076,6 +5163,48 @@ Promise.all([fetchRpkiAspaFeed(), fetchAllAtlasProbes(), fetchPdbOrgCountries()]
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// bio-rd RIB WebSocket — live route streaming on /ws/rib
|
||||
// ============================================================
|
||||
let WebSocketServer = null;
|
||||
try { WebSocketServer = require('ws').Server; } catch(_e) {}
|
||||
|
||||
if (WebSocketServer) {
|
||||
const ribWss = new WebSocketServer({ server, path: '/ws/rib' });
|
||||
ribWss.on('connection', function(ws) {
|
||||
let cancelStream = null;
|
||||
|
||||
ws.on('message', function(raw) {
|
||||
try {
|
||||
const msg = JSON.parse(raw);
|
||||
if (msg.type === 'rib-subscribe') {
|
||||
if (cancelStream) { cancelStream(); cancelStream = null; }
|
||||
if (!risClient) {
|
||||
ws.send(JSON.stringify({ type: 'error', error: 'bio-rd RIS not configured' }));
|
||||
return;
|
||||
}
|
||||
const router = msg.router || 'default';
|
||||
cancelStream = risClient.observeRib(
|
||||
router,
|
||||
'default',
|
||||
function(update) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(update)); },
|
||||
function(err) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'error', error: err.message })); }
|
||||
);
|
||||
}
|
||||
} catch(e) {
|
||||
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'error', error: e.message }));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', function() {
|
||||
if (cancelStream) { cancelStream(); cancelStream = null; }
|
||||
});
|
||||
});
|
||||
console.log('[bio-rd] RIB WebSocket server listening on /ws/rib');
|
||||
} else {
|
||||
console.log('[bio-rd] WebSocket server skipped (ws package not installed)');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Refresh timers — jittered to avoid thundering herd
|
||||
// ============================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user