@fibscope/agent 0.1.0 → 0.1.2
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/README.md +14 -7
- package/index.mjs +26 -12
- package/lib/public-peer-address.mjs +54 -0
- package/package.json +2 -2
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 hosted gateway returns the public IPv4 it observes for the authenticated agent request. The agent combines that IP with the local FNN P2P port and reports the resulting public multiaddr. Loopback, wildcard, private, and documentation addresses are never published. Set `peerAddress` only when intentionally overriding automatic discovery with another publicly dialable multiaddr.
|
|
53
52
|
|
|
54
53
|
## Commands
|
|
55
54
|
|
|
@@ -75,7 +74,7 @@ The agent pushes to `POST /agent/v1/report` with:
|
|
|
75
74
|
|
|
76
75
|
```json
|
|
77
76
|
{
|
|
78
|
-
"agentVersion": "0.1.
|
|
77
|
+
"agentVersion": "0.1.2",
|
|
79
78
|
"chain": "testnet",
|
|
80
79
|
"collectedAt": 1710000000000,
|
|
81
80
|
"node": {},
|
|
@@ -105,12 +104,20 @@ Collection and transmission are independent loops:
|
|
|
105
104
|
|
|
106
105
|
The local status file is `.fiber/agent-status.json`.
|
|
107
106
|
|
|
107
|
+
## Run FNN With PM2
|
|
108
|
+
|
|
109
|
+
Create the working directory, install setup and PM2, then run the setup process under PM2 with one command:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
mkdir -p "$HOME/fibscope" && cd "$HOME/fibscope" && npm install -g pm2 @fibscope/setup && rm -f "$(npm prefix -g)/bin/fnn" && FIBER_SECRET_KEY_PASSWORD='<your-password>' pm2 start "$(command -v setup)" --name fiber-testnet --interpreter node -- --network testnet --rpc-host 127.0.0.1 --rpc-port 8227 --p2p-port 8228 && pm2 save
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The Agent Setup page generates this command from a masked password field. The `fnn` link removal avoids confusing the package helper with the downloaded Fiber binary.
|
|
116
|
+
|
|
108
117
|
## NPM Usage
|
|
109
118
|
|
|
110
|
-
```
|
|
111
|
-
npm install -g @fibscope/agent
|
|
112
|
-
agent init
|
|
113
|
-
agent start
|
|
119
|
+
```bash
|
|
120
|
+
npm install -g @fibscope/agent@0.1.2 pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter node -- start && pm2 save
|
|
114
121
|
```
|
|
115
122
|
|
|
116
123
|
Or run without a global install:
|
package/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
5
5
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
@@ -31,8 +31,9 @@ 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, peerTcpPort, publishablePeerAddresses } from "./lib/public-peer-address.mjs";
|
|
34
35
|
|
|
35
|
-
const AGENT_VERSION = "0.1.
|
|
36
|
+
const AGENT_VERSION = "0.1.2";
|
|
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;
|
|
@@ -367,6 +368,8 @@ export async function createRuntime(options = {}) {
|
|
|
367
368
|
const config = await loadConfig(configPath, options);
|
|
368
369
|
await ensureFnnPreflight(config, statusPath, options);
|
|
369
370
|
const remoteConfig = await fetchAgentConfig(config).catch(() => undefined);
|
|
371
|
+
config.observedPublicIp = remoteConfig?.observedPublicIp ?? config.observedPublicIp;
|
|
372
|
+
config.observedPeerAddress = remoteConfig?.observedPeerAddress ?? config.observedPeerAddress;
|
|
370
373
|
if (!config.nodeDisplayName && remoteConfig?.node?.displayName) {
|
|
371
374
|
config.nodeDisplayName = remoteConfig.node.displayName;
|
|
372
375
|
}
|
|
@@ -499,19 +502,29 @@ export async function collectReport(config, previousReport = undefined) {
|
|
|
499
502
|
async function enrichReportNode(node, config) {
|
|
500
503
|
if (!node || typeof node !== "object") return node;
|
|
501
504
|
const displayName = textValue(config.nodeDisplayName);
|
|
502
|
-
const
|
|
505
|
+
const candidates = uniqueStrings([
|
|
503
506
|
...readAddressList(node.addresses),
|
|
504
507
|
...readAddressList(node.address),
|
|
505
508
|
...configuredPeerAddresses(config),
|
|
506
509
|
...await inferPeerAddressesFromLocalFnn(config),
|
|
507
510
|
]);
|
|
508
|
-
|
|
509
|
-
|
|
511
|
+
const addresses = publishablePeerAddresses([
|
|
512
|
+
config.observedPeerAddress,
|
|
513
|
+
buildObservedPeerAddress(config.observedPublicIp, [config.fnnP2pListenAddr, ...candidates]),
|
|
514
|
+
...candidates,
|
|
515
|
+
]);
|
|
516
|
+
const enriched = {
|
|
510
517
|
...node,
|
|
511
518
|
...(displayName ? { displayName } : {}),
|
|
512
|
-
address: node.address ?? addresses[0],
|
|
513
|
-
addresses,
|
|
514
519
|
};
|
|
520
|
+
if (addresses.length) {
|
|
521
|
+
enriched.address = addresses[0];
|
|
522
|
+
enriched.addresses = addresses;
|
|
523
|
+
} else {
|
|
524
|
+
delete enriched.address;
|
|
525
|
+
delete enriched.addresses;
|
|
526
|
+
}
|
|
527
|
+
return enriched;
|
|
515
528
|
}
|
|
516
529
|
|
|
517
530
|
function configuredPeerAddresses(config) {
|
|
@@ -2301,7 +2314,11 @@ function resolveReportUrl(config = {}) {
|
|
|
2301
2314
|
}
|
|
2302
2315
|
|
|
2303
2316
|
async function fetchAgentConfig(config) {
|
|
2304
|
-
const configUrl = config.configUrl ?? config.reportUrl.replace(/\/report\/?$/, "/config");
|
|
2317
|
+
const configUrl = new URL(config.configUrl ?? config.reportUrl.replace(/\/report\/?$/, "/config"));
|
|
2318
|
+
configUrl.searchParams.set("p2pPort", String(peerTcpPort([
|
|
2319
|
+
config.fnnP2pListenAddr,
|
|
2320
|
+
...configuredPeerAddresses(config),
|
|
2321
|
+
], 8228)));
|
|
2305
2322
|
const response = await fetch(configUrl, {
|
|
2306
2323
|
headers: {
|
|
2307
2324
|
"x-agent-token": config.agentToken,
|
|
@@ -2539,15 +2556,12 @@ async function enrichConfigFromMatchingFnn(config) {
|
|
|
2539
2556
|
}
|
|
2540
2557
|
|
|
2541
2558
|
function applyFnnMatchToAgentConfig(config, match) {
|
|
2542
|
-
const peerAddress = normalizeLocalListenAddress(match?.p2pListenAddr);
|
|
2543
|
-
const hasConfiguredPeerAddress = configuredPeerAddresses(config).length > 0;
|
|
2544
2559
|
return {
|
|
2545
2560
|
...config,
|
|
2546
2561
|
ckbRpcUrl: config.ckbRpcUrl ?? match.ckbRpcUrl,
|
|
2547
2562
|
fnnBaseDir: config.fnnBaseDir ?? match.baseDir,
|
|
2548
2563
|
fnnConfigPath: config.fnnConfigPath ?? match.configPath,
|
|
2549
2564
|
fnnP2pListenAddr: config.fnnP2pListenAddr ?? match.p2pListenAddr,
|
|
2550
|
-
...(!hasConfiguredPeerAddress && peerAddress ? { peerAddress } : {}),
|
|
2551
2565
|
chain: config.chain ?? match.network,
|
|
2552
2566
|
};
|
|
2553
2567
|
}
|
|
@@ -3227,7 +3241,7 @@ async function promptLine(label) {
|
|
|
3227
3241
|
}
|
|
3228
3242
|
}
|
|
3229
3243
|
|
|
3230
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
3244
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
3231
3245
|
const { command, options } = parseArgs(process.argv.slice(2));
|
|
3232
3246
|
const commands = { init, start, once, status, help };
|
|
3233
3247
|
if (!commands[command]) {
|
|
@@ -0,0 +1,54 @@
|
|
|
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 function peerTcpPort(values, fallback = 8228) {
|
|
9
|
+
for (const value of flatStrings(values)) {
|
|
10
|
+
const match = value.match(/\/tcp\/(\d{1,5})(?:\/|$)/i);
|
|
11
|
+
const port = Number(match?.[1]);
|
|
12
|
+
if (Number.isInteger(port) && port > 0 && port <= 65_535) return port;
|
|
13
|
+
}
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function publishablePeerAddresses(values) {
|
|
18
|
+
return [...new Set(flatStrings(values).filter(isPublishablePeerAddress))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isPublishablePeerAddress(value) {
|
|
22
|
+
const address = String(value ?? "").trim();
|
|
23
|
+
if (!address) return false;
|
|
24
|
+
const ipv4 = /\/ip4\/([^/]+)/i.exec(address)?.[1];
|
|
25
|
+
if (ipv4) return Boolean(normalizePublicIpv4(ipv4));
|
|
26
|
+
const ipv6 = /\/ip6\/([^/]+)/i.exec(address)?.[1]?.toLowerCase();
|
|
27
|
+
if (ipv6) {
|
|
28
|
+
return ipv6 !== "::" && ipv6 !== "::1" && !ipv6.startsWith("fc") && !ipv6.startsWith("fd") && !/^fe[89ab]/.test(ipv6);
|
|
29
|
+
}
|
|
30
|
+
return /\/(?:dns|dns4|dns6)\/[^/]+/i.test(address);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizePublicIpv4(value) {
|
|
34
|
+
let candidate = String(value ?? "").trim();
|
|
35
|
+
if (candidate.startsWith("::ffff:")) candidate = candidate.slice(7);
|
|
36
|
+
const parts = candidate.split(".").map(Number);
|
|
37
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return undefined;
|
|
38
|
+
const [a, b, c] = parts;
|
|
39
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224) return undefined;
|
|
40
|
+
if (a === 100 && b >= 64 && b <= 127) return undefined;
|
|
41
|
+
if (a === 169 && b === 254) return undefined;
|
|
42
|
+
if (a === 172 && b >= 16 && b <= 31) return undefined;
|
|
43
|
+
if (a === 192 && b === 168) return undefined;
|
|
44
|
+
if (a === 198 && (b === 18 || b === 19)) return undefined;
|
|
45
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2)) return undefined;
|
|
46
|
+
if (a === 198 && b === 51 && c === 100) return undefined;
|
|
47
|
+
if (a === 203 && b === 0 && c === 113) return undefined;
|
|
48
|
+
return parts.join(".");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function flatStrings(values) {
|
|
52
|
+
const input = Array.isArray(values) ? values.flat(Infinity) : [values];
|
|
53
|
+
return input.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fibscope/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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": {
|