@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 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
- `peerAddress` is the Fiber dial address advertised to Fibscope for public routing and channel creation. For local duo development this is usually a loopback multiaddr like `/ip4/127.0.0.1/tcp/8228`; for an internet-facing public node, set it to an address other nodes can actually dial.
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.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
- ```powershell
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.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 addresses = uniqueStrings([
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
- if (!addresses.length && !displayName) return node;
509
- return {
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.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": {