@fibscope/agent 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/README.md +6 -7
- package/index.mjs +48 -10
- package/lib/public-peer-address.mjs +92 -0
- package/package.json +2 -2
package/.env.example
CHANGED
|
@@ -5,3 +5,5 @@ FIBSCOPE_AGENT_CHAIN=testnet
|
|
|
5
5
|
FIBSCOPE_AGENT_FIBER_RPC_URL=http://127.0.0.1:8227
|
|
6
6
|
FIBSCOPE_AGENT_PUSH_INTERVAL_MS=15000
|
|
7
7
|
FIBSCOPE_AGENT_PUBLIC_PROFILE=false
|
|
8
|
+
# Optional override; by default the agent discovers its own public IPv4.
|
|
9
|
+
# FIBSCOPE_AGENT_PUBLIC_IP=54.179.226.154
|
package/README.md
CHANGED
|
@@ -32,7 +32,6 @@ The agent also supports `.fiber/agent.json`:
|
|
|
32
32
|
"agentToken": "raw-token-issued-by-platform",
|
|
33
33
|
"chain": "testnet",
|
|
34
34
|
"fiberRpcUrl": "http://127.0.0.1:8247",
|
|
35
|
-
"peerAddress": "/ip4/127.0.0.1/tcp/8248",
|
|
36
35
|
"pushIntervalMs": 15000,
|
|
37
36
|
"publicProfileEnabled": true,
|
|
38
37
|
"overwriteNodeRegistration": false,
|
|
@@ -49,7 +48,7 @@ The agent also supports `.fiber/agent.json`:
|
|
|
49
48
|
}
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
`
|
|
51
|
+
The agent discovers its own public IPv4 through `https://api.ipify.org`, combines it with the local FNN P2P port, and includes both the IP and resulting public multiaddr in registration and report requests. Loopback, wildcard, private, and documentation addresses are never published. Set `FIBSCOPE_AGENT_PUBLIC_IP` only when intentionally overriding automatic discovery, or `FIBSCOPE_AGENT_PUBLIC_IP_URL` to use another compatible JSON or plain-text discovery endpoint.
|
|
53
52
|
|
|
54
53
|
## Commands
|
|
55
54
|
|
|
@@ -75,9 +74,11 @@ The agent pushes to `POST /agent/v1/report` with:
|
|
|
75
74
|
|
|
76
75
|
```json
|
|
77
76
|
{
|
|
78
|
-
"agentVersion": "0.1.
|
|
77
|
+
"agentVersion": "0.1.3",
|
|
79
78
|
"chain": "testnet",
|
|
80
79
|
"collectedAt": 1710000000000,
|
|
80
|
+
"publicIp": "54.179.226.154",
|
|
81
|
+
"peerAddress": "/ip4/54.179.226.154/tcp/8228",
|
|
81
82
|
"node": {},
|
|
82
83
|
"peers": [],
|
|
83
84
|
"channels": [],
|
|
@@ -117,10 +118,8 @@ The Agent Setup page generates this command from a masked password field. The `f
|
|
|
117
118
|
|
|
118
119
|
## NPM Usage
|
|
119
120
|
|
|
120
|
-
```
|
|
121
|
-
npm install -g @fibscope/agent
|
|
122
|
-
agent init
|
|
123
|
-
agent start
|
|
121
|
+
```bash
|
|
122
|
+
npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter node -- start && pm2 save
|
|
124
123
|
```
|
|
125
124
|
|
|
126
125
|
Or run without a global install:
|
package/index.mjs
CHANGED
|
@@ -31,14 +31,17 @@ import { createTelemetryCollector } from "./lib/observe/collector.mjs";
|
|
|
31
31
|
import { recommend } from "./lib/bridge/recommendation.mjs";
|
|
32
32
|
import { virtualizePayment } from "./lib/route/route.mjs";
|
|
33
33
|
import { chooseFnnInstance, describeFnnInstance, discoverFnnInstances, rpcResponds } from "./lib/fnn-discovery.mjs";
|
|
34
|
+
import { buildObservedPeerAddress, discoverPublicIpv4, publishablePeerAddresses } from "./lib/public-peer-address.mjs";
|
|
34
35
|
|
|
35
|
-
const AGENT_VERSION = "0.1.
|
|
36
|
+
const AGENT_VERSION = "0.1.3";
|
|
36
37
|
const DEFAULT_CONFIG_PATH = ".fiber/agent.json";
|
|
37
38
|
const DEFAULT_STATUS_PATH = ".fiber/agent-status.json";
|
|
38
39
|
const DEFAULT_PUSH_INTERVAL_MS = 15_000;
|
|
39
40
|
const MAX_BACKOFF_MS = 300_000;
|
|
40
41
|
const DEFAULT_PULSE_MODULE = new URL("./lib/pulse/index.mjs", import.meta.url).href;
|
|
41
42
|
const DEFAULT_FNN_PREFLIGHT_TIMEOUT_MS = 2_500;
|
|
43
|
+
const DEFAULT_PUBLIC_IP_DISCOVERY_URL = "https://api.ipify.org?format=json";
|
|
44
|
+
const DEFAULT_PUBLIC_IP_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
42
45
|
const DEFAULT_REPORT_URL = "https://ejchwtvdxpyojxsursmh.supabase.co/functions/v1/fibscope-gateway/agent/v1/report";
|
|
43
46
|
const AGENT_ENV_PATHS = [resolve("agent/.env"), resolve(".env")];
|
|
44
47
|
const ENV_REPORT_URL = "FIBGATE";
|
|
@@ -366,6 +369,7 @@ export async function createRuntime(options = {}) {
|
|
|
366
369
|
const statusPath = resolve(options.status ?? DEFAULT_STATUS_PATH);
|
|
367
370
|
const config = await loadConfig(configPath, options);
|
|
368
371
|
await ensureFnnPreflight(config, statusPath, options);
|
|
372
|
+
await resolveAgentPublicIdentity(config);
|
|
369
373
|
const remoteConfig = await fetchAgentConfig(config).catch(() => undefined);
|
|
370
374
|
if (!config.nodeDisplayName && remoteConfig?.node?.displayName) {
|
|
371
375
|
config.nodeDisplayName = remoteConfig.node.displayName;
|
|
@@ -478,6 +482,8 @@ export async function collectReport(config, previousReport = undefined) {
|
|
|
478
482
|
agentVersion: AGENT_VERSION,
|
|
479
483
|
chain: config.chain ?? "testnet",
|
|
480
484
|
collectedAt,
|
|
485
|
+
publicIp: config.publicIp,
|
|
486
|
+
peerAddress: config.publicPeerAddress,
|
|
481
487
|
node: state.node,
|
|
482
488
|
peers: state.peers ?? [],
|
|
483
489
|
channels: state.channels ?? [],
|
|
@@ -499,19 +505,29 @@ export async function collectReport(config, previousReport = undefined) {
|
|
|
499
505
|
async function enrichReportNode(node, config) {
|
|
500
506
|
if (!node || typeof node !== "object") return node;
|
|
501
507
|
const displayName = textValue(config.nodeDisplayName);
|
|
502
|
-
const
|
|
508
|
+
const candidates = uniqueStrings([
|
|
503
509
|
...readAddressList(node.addresses),
|
|
504
510
|
...readAddressList(node.address),
|
|
505
511
|
...configuredPeerAddresses(config),
|
|
506
512
|
...await inferPeerAddressesFromLocalFnn(config),
|
|
507
513
|
]);
|
|
508
|
-
|
|
509
|
-
|
|
514
|
+
const addresses = publishablePeerAddresses([
|
|
515
|
+
config.publicPeerAddress,
|
|
516
|
+
buildObservedPeerAddress(config.publicIp, [config.fnnP2pListenAddr, ...candidates]),
|
|
517
|
+
...candidates,
|
|
518
|
+
]);
|
|
519
|
+
const enriched = {
|
|
510
520
|
...node,
|
|
511
521
|
...(displayName ? { displayName } : {}),
|
|
512
|
-
address: node.address ?? addresses[0],
|
|
513
|
-
addresses,
|
|
514
522
|
};
|
|
523
|
+
if (addresses.length) {
|
|
524
|
+
enriched.address = addresses[0];
|
|
525
|
+
enriched.addresses = addresses;
|
|
526
|
+
} else {
|
|
527
|
+
delete enriched.address;
|
|
528
|
+
delete enriched.addresses;
|
|
529
|
+
}
|
|
530
|
+
return enriched;
|
|
515
531
|
}
|
|
516
532
|
|
|
517
533
|
function configuredPeerAddresses(config) {
|
|
@@ -2281,6 +2297,8 @@ async function loadConfig(configPath, options = {}) {
|
|
|
2281
2297
|
ckbIndexerUrl: process.env.FIBSCOPE_AGENT_CKB_INDEXER_URL,
|
|
2282
2298
|
walletPath: process.env.FIBSCOPE_AGENT_WALLET_PATH ?? envValue(ENV_WALLET_PATH),
|
|
2283
2299
|
peerAddress: process.env.FIBSCOPE_AGENT_PEER_ADDRESS,
|
|
2300
|
+
publicIp: process.env.FIBSCOPE_AGENT_PUBLIC_IP,
|
|
2301
|
+
publicIpDiscoveryUrl: process.env.FIBSCOPE_AGENT_PUBLIC_IP_URL,
|
|
2284
2302
|
};
|
|
2285
2303
|
label = "agent/.env";
|
|
2286
2304
|
} else if (!existsSync(configPath)) {
|
|
@@ -2301,7 +2319,7 @@ function resolveReportUrl(config = {}) {
|
|
|
2301
2319
|
}
|
|
2302
2320
|
|
|
2303
2321
|
async function fetchAgentConfig(config) {
|
|
2304
|
-
const configUrl = config.configUrl ?? config.reportUrl.replace(/\/report\/?$/, "/config");
|
|
2322
|
+
const configUrl = new URL(config.configUrl ?? config.reportUrl.replace(/\/report\/?$/, "/config"));
|
|
2305
2323
|
const response = await fetch(configUrl, {
|
|
2306
2324
|
headers: {
|
|
2307
2325
|
"x-agent-token": config.agentToken,
|
|
@@ -2351,6 +2369,8 @@ async function registerAgentNode(config, report, remoteConfig) {
|
|
|
2351
2369
|
displayName: config.nodeDisplayName ?? remoteConfig?.node?.displayName,
|
|
2352
2370
|
publicProfileEnabled: Boolean(report.publicProfileEnabled),
|
|
2353
2371
|
overwrite: Boolean(config.overwriteNodeRegistration),
|
|
2372
|
+
publicIp: report.publicIp ?? config.publicIp,
|
|
2373
|
+
peerAddress: report.peerAddress ?? config.publicPeerAddress,
|
|
2354
2374
|
node: report.node ?? {},
|
|
2355
2375
|
}),
|
|
2356
2376
|
});
|
|
@@ -2488,6 +2508,8 @@ function normalizeConfig(config, label) {
|
|
|
2488
2508
|
ckbRpcUrl: process.env.FIBSCOPE_AGENT_CKB_RPC_URL ?? envValue(ENV_CKB_RPC_URL) ?? config.ckbRpcUrl,
|
|
2489
2509
|
ckbIndexerUrl: process.env.FIBSCOPE_AGENT_CKB_INDEXER_URL ?? config.ckbIndexerUrl,
|
|
2490
2510
|
walletPath: process.env.FIBSCOPE_AGENT_WALLET_PATH ?? envValue(ENV_WALLET_PATH) ?? config.walletPath,
|
|
2511
|
+
publicIp: process.env.FIBSCOPE_AGENT_PUBLIC_IP ?? config.publicIp,
|
|
2512
|
+
publicIpDiscoveryUrl: process.env.FIBSCOPE_AGENT_PUBLIC_IP_URL ?? config.publicIpDiscoveryUrl,
|
|
2491
2513
|
pushIntervalMs: positiveNumber(config.pushIntervalMs, DEFAULT_PUSH_INTERVAL_MS),
|
|
2492
2514
|
commandPollMs: positiveNumber(config.commandPollMs, DEFAULT_COMMAND_POLL_MS),
|
|
2493
2515
|
knownPeerReconnectMs: positiveNumber(config.knownPeerReconnectMs, DEFAULT_KNOWN_PEER_RECONNECT_MS),
|
|
@@ -2499,6 +2521,25 @@ function normalizeConfig(config, label) {
|
|
|
2499
2521
|
return normalized;
|
|
2500
2522
|
}
|
|
2501
2523
|
|
|
2524
|
+
async function resolveAgentPublicIdentity(config) {
|
|
2525
|
+
try {
|
|
2526
|
+
const publicIp = await discoverPublicIpv4({
|
|
2527
|
+
publicIp: config.publicIp,
|
|
2528
|
+
endpoint: config.publicIpDiscoveryUrl ?? DEFAULT_PUBLIC_IP_DISCOVERY_URL,
|
|
2529
|
+
timeoutMs: config.publicIpDiscoveryTimeoutMs ?? DEFAULT_PUBLIC_IP_DISCOVERY_TIMEOUT_MS,
|
|
2530
|
+
});
|
|
2531
|
+
config.publicIp = publicIp;
|
|
2532
|
+
config.publicPeerAddress = buildObservedPeerAddress(publicIp, [
|
|
2533
|
+
config.fnnP2pListenAddr,
|
|
2534
|
+
...configuredPeerAddresses(config),
|
|
2535
|
+
]);
|
|
2536
|
+
} catch (error) {
|
|
2537
|
+
console.warn(`Fibscope agent public IP unavailable: ${message(error)}`);
|
|
2538
|
+
config.publicIp = undefined;
|
|
2539
|
+
config.publicPeerAddress = publishablePeerAddresses(configuredPeerAddresses(config))[0];
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2502
2543
|
async function bindAgentConfigToFnn(config, label, options = {}) {
|
|
2503
2544
|
const next = { ...config };
|
|
2504
2545
|
if (options.fiberRpcUrl) {
|
|
@@ -2539,15 +2580,12 @@ async function enrichConfigFromMatchingFnn(config) {
|
|
|
2539
2580
|
}
|
|
2540
2581
|
|
|
2541
2582
|
function applyFnnMatchToAgentConfig(config, match) {
|
|
2542
|
-
const peerAddress = normalizeLocalListenAddress(match?.p2pListenAddr);
|
|
2543
|
-
const hasConfiguredPeerAddress = configuredPeerAddresses(config).length > 0;
|
|
2544
2583
|
return {
|
|
2545
2584
|
...config,
|
|
2546
2585
|
ckbRpcUrl: config.ckbRpcUrl ?? match.ckbRpcUrl,
|
|
2547
2586
|
fnnBaseDir: config.fnnBaseDir ?? match.baseDir,
|
|
2548
2587
|
fnnConfigPath: config.fnnConfigPath ?? match.configPath,
|
|
2549
2588
|
fnnP2pListenAddr: config.fnnP2pListenAddr ?? match.p2pListenAddr,
|
|
2550
|
-
...(!hasConfiguredPeerAddress && peerAddress ? { peerAddress } : {}),
|
|
2551
2589
|
chain: config.chain ?? match.network,
|
|
2552
2590
|
};
|
|
2553
2591
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export function buildObservedPeerAddress(observedPublicIp, candidates = [], fallbackPort = 8228) {
|
|
2
|
+
const ip = normalizePublicIpv4(observedPublicIp);
|
|
3
|
+
if (!ip) return undefined;
|
|
4
|
+
const port = peerTcpPort(candidates, fallbackPort);
|
|
5
|
+
return `/ip4/${ip}/tcp/${port}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function discoverPublicIpv4(options = {}) {
|
|
9
|
+
const configured = normalizePublicIpv4(options.publicIp);
|
|
10
|
+
if (configured) return configured;
|
|
11
|
+
|
|
12
|
+
const endpoint = String(options.endpoint ?? "https://api.ipify.org?format=json").trim();
|
|
13
|
+
if (!endpoint) throw new Error("Public IPv4 discovery endpoint is not configured.");
|
|
14
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 5_000);
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
try {
|
|
18
|
+
const response = await (options.fetchImpl ?? fetch)(endpoint, {
|
|
19
|
+
headers: { accept: "application/json, text/plain;q=0.9" },
|
|
20
|
+
signal: controller.signal,
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
23
|
+
const body = await response.text();
|
|
24
|
+
let candidate = body;
|
|
25
|
+
try {
|
|
26
|
+
candidate = JSON.parse(body)?.ip ?? body;
|
|
27
|
+
} catch {
|
|
28
|
+
// Configurable plain-text IP endpoints are supported too.
|
|
29
|
+
}
|
|
30
|
+
const publicIp = normalizePublicIpv4(candidate);
|
|
31
|
+
if (!publicIp) throw new Error("response did not contain a public IPv4 address");
|
|
32
|
+
return publicIp;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
35
|
+
throw new Error(`Public IPv4 discovery failed at ${endpoint}: ${detail}`);
|
|
36
|
+
} finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function peerTcpPort(values, fallback = 8228) {
|
|
42
|
+
for (const value of flatStrings(values)) {
|
|
43
|
+
const match = value.match(/\/tcp\/(\d{1,5})(?:\/|$)/i);
|
|
44
|
+
const port = Number(match?.[1]);
|
|
45
|
+
if (Number.isInteger(port) && port > 0 && port <= 65_535) return port;
|
|
46
|
+
}
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function publishablePeerAddresses(values) {
|
|
51
|
+
return [...new Set(flatStrings(values).filter(isPublishablePeerAddress))];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isPublishablePeerAddress(value) {
|
|
55
|
+
const address = String(value ?? "").trim();
|
|
56
|
+
if (!address) return false;
|
|
57
|
+
const ipv4 = /\/ip4\/([^/]+)/i.exec(address)?.[1];
|
|
58
|
+
if (ipv4) return Boolean(normalizePublicIpv4(ipv4));
|
|
59
|
+
const ipv6 = /\/ip6\/([^/]+)/i.exec(address)?.[1]?.toLowerCase();
|
|
60
|
+
if (ipv6) {
|
|
61
|
+
return ipv6 !== "::" && ipv6 !== "::1" && !ipv6.startsWith("fc") && !ipv6.startsWith("fd") && !/^fe[89ab]/.test(ipv6);
|
|
62
|
+
}
|
|
63
|
+
return /\/(?:dns|dns4|dns6)\/[^/]+/i.test(address);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function normalizePublicIpv4(value) {
|
|
67
|
+
let candidate = String(value ?? "").trim();
|
|
68
|
+
if (candidate.startsWith("::ffff:")) candidate = candidate.slice(7);
|
|
69
|
+
const parts = candidate.split(".").map(Number);
|
|
70
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return undefined;
|
|
71
|
+
const [a, b, c] = parts;
|
|
72
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224) return undefined;
|
|
73
|
+
if (a === 100 && b >= 64 && b <= 127) return undefined;
|
|
74
|
+
if (a === 169 && b === 254) return undefined;
|
|
75
|
+
if (a === 172 && b >= 16 && b <= 31) return undefined;
|
|
76
|
+
if (a === 192 && b === 168) return undefined;
|
|
77
|
+
if (a === 198 && (b === 18 || b === 19)) return undefined;
|
|
78
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2)) return undefined;
|
|
79
|
+
if (a === 198 && b === 51 && c === 100) return undefined;
|
|
80
|
+
if (a === 203 && b === 0 && c === 113) return undefined;
|
|
81
|
+
return parts.join(".");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function flatStrings(values) {
|
|
85
|
+
const input = Array.isArray(values) ? values.flat(Infinity) : [values];
|
|
86
|
+
return input.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function positiveTimeout(value, fallback) {
|
|
90
|
+
const number = Number(value);
|
|
91
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fibscope/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fibscope Agent for Fiber node owners.",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
".env.example"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"check": "node --check index.mjs && node --check lib/pulse/index.mjs && node --check lib/pulse/engine.mjs && node --check lib/pulse/snapshot.mjs && node --check lib/pulse/intelligence.mjs && node --check lib/pulse/providers.mjs && node --check lib/pulse/server.mjs && node --check lib/observe/collector.mjs && node --check lib/bridge/recommendation.mjs && node --check lib/route/route.mjs && node --check lib/fnn-discovery.mjs",
|
|
19
|
+
"check": "node --check index.mjs && node --check lib/public-peer-address.mjs && node --check lib/pulse/index.mjs && node --check lib/pulse/engine.mjs && node --check lib/pulse/snapshot.mjs && node --check lib/pulse/intelligence.mjs && node --check lib/pulse/providers.mjs && node --check lib/pulse/server.mjs && node --check lib/observe/collector.mjs && node --check lib/bridge/recommendation.mjs && node --check lib/route/route.mjs && node --check lib/fnn-discovery.mjs && node --test test/*.test.mjs",
|
|
20
20
|
"pack:dry-run": "npm pack --dry-run"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|