@decentnetwork/lan 0.1.249 → 0.1.251
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/dist/cli/commands.d.ts +44 -0
- package/dist/cli/commands.js +206 -1
- package/dist/cli/index.js +35 -1
- package/dist/config/loader.d.ts +17 -1
- package/dist/config/loader.js +43 -3
- package/dist/console/console.js +25 -2
- package/dist/types.d.ts +12 -0
- package/dist/ui/desktop/app.js +1 -1
- package/package.json +2 -2
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI command handlers
|
|
3
3
|
*/
|
|
4
|
+
import type { BootstrapNode } from "../types.js";
|
|
4
5
|
/**
|
|
5
6
|
* Initialize ~/.agentnet directory and config
|
|
6
7
|
*/
|
|
@@ -516,3 +517,46 @@ export declare function cmdServiceRestart(args: {
|
|
|
516
517
|
export declare function cmdRestart(args: {
|
|
517
518
|
configDir?: string;
|
|
518
519
|
}): Promise<void>;
|
|
520
|
+
/**
|
|
521
|
+
* Parse the published feed's node array into our BootstrapNode shape.
|
|
522
|
+
*
|
|
523
|
+
* The feed uses the native SDK's field names (`ipv4`, string `port`,
|
|
524
|
+
* `publicKey`); we use `host`/number `port`/`pk`. Both spellings are accepted
|
|
525
|
+
* so the same command works against a hand-written file too.
|
|
526
|
+
*
|
|
527
|
+
* Invalid entries are skipped and reported rather than aborting the whole
|
|
528
|
+
* update — one bad row in the feed shouldn't block adopting the other 14.
|
|
529
|
+
*/
|
|
530
|
+
declare function parseFeedNodes(raw: unknown, rejected: string[]): BootstrapNode[];
|
|
531
|
+
/** Test seam — the feed parser is a trust boundary and gets pinned by
|
|
532
|
+
* test/unit/bootstrap-feed.test.ts without going over the network. */
|
|
533
|
+
export declare const __testParseFeedNodes: typeof parseFeedNodes;
|
|
534
|
+
/**
|
|
535
|
+
* `agentnet bootstrap update` — pull the published bootstrap list and adopt any
|
|
536
|
+
* node the installed build doesn't ship.
|
|
537
|
+
*
|
|
538
|
+
* Why this exists: `npm install -g @decentnetwork/lan@latest` updates the
|
|
539
|
+
* BUILT-IN defaults (config.yaml's own `bootstrapNodes:` block is ignored at
|
|
540
|
+
* load time — see ConfigLoader.normalizeConfig), so upgrading does refresh
|
|
541
|
+
* bootstraps. But that only ever gives you the list as of the last release. When
|
|
542
|
+
* we stand up a bootstrap mid-cycle, every node has to wait for an npm publish.
|
|
543
|
+
* This command closes that gap: fetch the live feed, append what's new to
|
|
544
|
+
* `carrier.extraBootstrapNodes`, and the next daemon start picks it up.
|
|
545
|
+
*/
|
|
546
|
+
export declare function cmdBootstrapUpdate(args: {
|
|
547
|
+
configDir?: string;
|
|
548
|
+
url?: string;
|
|
549
|
+
dryRun?: boolean;
|
|
550
|
+
clear?: boolean;
|
|
551
|
+
}): Promise<void>;
|
|
552
|
+
/**
|
|
553
|
+
* `agentnet bootstrap show` — print the list the daemon will actually use,
|
|
554
|
+
* flagging which entries are shipped defaults vs. adopted from a feed.
|
|
555
|
+
*
|
|
556
|
+
* Worth its own command because the `bootstrapNodes:` block sitting in
|
|
557
|
+
* config.yaml is NOT what gets used — it's overwritten by the shipped defaults
|
|
558
|
+
* on every load, so reading the file gives the wrong answer.
|
|
559
|
+
*/
|
|
560
|
+
export declare function cmdBootstrapShow(args: {
|
|
561
|
+
configDir?: string;
|
|
562
|
+
}): Promise<void>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, copyFileSync } from "fs";
|
|
|
6
6
|
import { createConnection } from "net";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { ConfigLoader, DEFAULT_DORAS, DEFAULT_EXITS } from "../config/loader.js";
|
|
9
|
+
import { ConfigLoader, DEFAULT_BOOTSTRAP_NODES, DEFAULT_DORAS, DEFAULT_EXITS, mergeBootstrapNodes, } from "../config/loader.js";
|
|
10
10
|
import { ipcSocketPath, ipcSocketSupportsFsExistenceCheck, } from "../daemon/ipc.js";
|
|
11
11
|
import { DaemonServer } from "../daemon/server.js";
|
|
12
12
|
import { Ipam } from "../ipam/ipam.js";
|
|
@@ -2543,3 +2543,208 @@ export async function cmdRestart(args) {
|
|
|
2543
2543
|
console.log(`Daemon scheduled for self-restart. New process will exec: ${(data.argv || []).join(" ")}`);
|
|
2544
2544
|
console.log("Allow ~5s for the daemon to come back up. Verify with 'agentnet diag'.");
|
|
2545
2545
|
}
|
|
2546
|
+
// ---------------------------------------------------------------------------
|
|
2547
|
+
// bootstrap feed
|
|
2548
|
+
// ---------------------------------------------------------------------------
|
|
2549
|
+
/**
|
|
2550
|
+
* Where the fleet's live bootstrap list is published. Same file the Beagle web
|
|
2551
|
+
* app reads, so app and daemon converge on one source of truth instead of
|
|
2552
|
+
* drifting apart (2026-07-25: the app's list had lens/gojipower/gfax while the
|
|
2553
|
+
* npm defaults didn't, and vice versa).
|
|
2554
|
+
*/
|
|
2555
|
+
const DEFAULT_BOOTSTRAP_FEED_URL = "https://beagle.chat/assets/bgservers.json";
|
|
2556
|
+
/** Carrier public keys are base58-encoded 32-byte keys → 43-44 chars. */
|
|
2557
|
+
const BASE58_KEY_RE = /^[1-9A-HJ-NP-Za-km-z]{40,50}$/;
|
|
2558
|
+
/** IPv4 literal or hostname. Deliberately strict: this is remote input that
|
|
2559
|
+
* becomes network config, so anything with a scheme, path, port suffix,
|
|
2560
|
+
* whitespace or credentials is rejected rather than normalized. */
|
|
2561
|
+
const HOST_RE = /^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/;
|
|
2562
|
+
/** Upper bound on how many nodes a feed may install. A bootstrap list is a
|
|
2563
|
+
* dozen or so entries; anything near this is a malformed or hostile feed. */
|
|
2564
|
+
const MAX_FEED_NODES = 64;
|
|
2565
|
+
/**
|
|
2566
|
+
* Parse the published feed's node array into our BootstrapNode shape.
|
|
2567
|
+
*
|
|
2568
|
+
* The feed uses the native SDK's field names (`ipv4`, string `port`,
|
|
2569
|
+
* `publicKey`); we use `host`/number `port`/`pk`. Both spellings are accepted
|
|
2570
|
+
* so the same command works against a hand-written file too.
|
|
2571
|
+
*
|
|
2572
|
+
* Invalid entries are skipped and reported rather than aborting the whole
|
|
2573
|
+
* update — one bad row in the feed shouldn't block adopting the other 14.
|
|
2574
|
+
*/
|
|
2575
|
+
function parseFeedNodes(raw, rejected) {
|
|
2576
|
+
if (!Array.isArray(raw))
|
|
2577
|
+
return [];
|
|
2578
|
+
const out = [];
|
|
2579
|
+
for (const entry of raw.slice(0, MAX_FEED_NODES)) {
|
|
2580
|
+
if (!entry || typeof entry !== "object") {
|
|
2581
|
+
rejected.push(`${JSON.stringify(entry)} (not an object)`);
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
const e = entry;
|
|
2585
|
+
const host = typeof e.ipv4 === "string" ? e.ipv4 : typeof e.host === "string" ? e.host : "";
|
|
2586
|
+
const portRaw = e.port;
|
|
2587
|
+
const port = typeof portRaw === "number"
|
|
2588
|
+
? portRaw
|
|
2589
|
+
: typeof portRaw === "string"
|
|
2590
|
+
? Number.parseInt(portRaw, 10)
|
|
2591
|
+
: NaN;
|
|
2592
|
+
const pk = typeof e.publicKey === "string" ? e.publicKey : typeof e.pk === "string" ? e.pk : "";
|
|
2593
|
+
if (!HOST_RE.test(host)) {
|
|
2594
|
+
rejected.push(`${host || "(no host)"} — bad host`);
|
|
2595
|
+
continue;
|
|
2596
|
+
}
|
|
2597
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2598
|
+
rejected.push(`${host} — bad port ${String(portRaw)}`);
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (!BASE58_KEY_RE.test(pk)) {
|
|
2602
|
+
rejected.push(`${host}:${port} — bad public key`);
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
out.push({ host, port, pk });
|
|
2606
|
+
}
|
|
2607
|
+
if (Array.isArray(raw) && raw.length > MAX_FEED_NODES) {
|
|
2608
|
+
rejected.push(`(${raw.length - MAX_FEED_NODES} further entries ignored — feed over ${MAX_FEED_NODES} nodes)`);
|
|
2609
|
+
}
|
|
2610
|
+
return out;
|
|
2611
|
+
}
|
|
2612
|
+
/** Test seam — the feed parser is a trust boundary and gets pinned by
|
|
2613
|
+
* test/unit/bootstrap-feed.test.ts without going over the network. */
|
|
2614
|
+
export const __testParseFeedNodes = parseFeedNodes;
|
|
2615
|
+
/**
|
|
2616
|
+
* `agentnet bootstrap update` — pull the published bootstrap list and adopt any
|
|
2617
|
+
* node the installed build doesn't ship.
|
|
2618
|
+
*
|
|
2619
|
+
* Why this exists: `npm install -g @decentnetwork/lan@latest` updates the
|
|
2620
|
+
* BUILT-IN defaults (config.yaml's own `bootstrapNodes:` block is ignored at
|
|
2621
|
+
* load time — see ConfigLoader.normalizeConfig), so upgrading does refresh
|
|
2622
|
+
* bootstraps. But that only ever gives you the list as of the last release. When
|
|
2623
|
+
* we stand up a bootstrap mid-cycle, every node has to wait for an npm publish.
|
|
2624
|
+
* This command closes that gap: fetch the live feed, append what's new to
|
|
2625
|
+
* `carrier.extraBootstrapNodes`, and the next daemon start picks it up.
|
|
2626
|
+
*/
|
|
2627
|
+
export async function cmdBootstrapUpdate(args) {
|
|
2628
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
2629
|
+
const configPath = resolve(dir, "config.yaml");
|
|
2630
|
+
const config = await ConfigLoader.load(configPath);
|
|
2631
|
+
if (args.clear) {
|
|
2632
|
+
const had = config.carrier.extraBootstrapNodes?.length ?? 0;
|
|
2633
|
+
delete config.carrier.extraBootstrapNodes;
|
|
2634
|
+
delete config.carrier.bootstrapSource;
|
|
2635
|
+
config.carrier.bootstrapNodes = [...DEFAULT_BOOTSTRAP_NODES];
|
|
2636
|
+
await ConfigLoader.save(config, configPath);
|
|
2637
|
+
console.log(`Cleared ${had} extra bootstrap node(s); back to the ${DEFAULT_BOOTSTRAP_NODES.length} shipped defaults.`);
|
|
2638
|
+
console.log("Takes effect on the next daemon start ('agentnet restart').");
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
const url = args.url || DEFAULT_BOOTSTRAP_FEED_URL;
|
|
2642
|
+
// Remote config over plain HTTP is trivially rewritable in transit, and the
|
|
2643
|
+
// payload here decides which nodes we hand our DHT traffic to.
|
|
2644
|
+
if (!url.startsWith("https://")) {
|
|
2645
|
+
console.error(`Refusing to fetch bootstrap config over a non-HTTPS URL: ${url}`);
|
|
2646
|
+
process.exit(1);
|
|
2647
|
+
}
|
|
2648
|
+
console.log(`Fetching ${url} ...`);
|
|
2649
|
+
let payload;
|
|
2650
|
+
try {
|
|
2651
|
+
const res = await fetch(url, {
|
|
2652
|
+
signal: AbortSignal.timeout(20_000),
|
|
2653
|
+
headers: { accept: "application/json" },
|
|
2654
|
+
});
|
|
2655
|
+
if (!res.ok)
|
|
2656
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
2657
|
+
payload = await res.json();
|
|
2658
|
+
}
|
|
2659
|
+
catch (error) {
|
|
2660
|
+
// A feed that's down or unreachable is an ordinary outcome (offline node,
|
|
2661
|
+
// GFW, publish in flight) — report it and leave the config alone. The
|
|
2662
|
+
// daemon keeps running on the shipped defaults either way.
|
|
2663
|
+
console.error(`Could not fetch the bootstrap feed (${url}): ${error instanceof Error ? error.message : String(error)}`);
|
|
2664
|
+
console.error("Config unchanged — the built-in defaults still apply.");
|
|
2665
|
+
process.exit(1);
|
|
2666
|
+
}
|
|
2667
|
+
const feed = payload;
|
|
2668
|
+
const rejected = [];
|
|
2669
|
+
const fetched = parseFeedNodes(feed?.bootstrapNodes, rejected);
|
|
2670
|
+
if (fetched.length === 0) {
|
|
2671
|
+
console.error(`Feed had no usable bootstrapNodes (${rejected.length} rejected).`);
|
|
2672
|
+
for (const bad of rejected.slice(0, 10))
|
|
2673
|
+
console.error(` ! ${bad}`);
|
|
2674
|
+
console.error("Config unchanged.");
|
|
2675
|
+
process.exit(1);
|
|
2676
|
+
}
|
|
2677
|
+
const shipped = new Set(DEFAULT_BOOTSTRAP_NODES.map((n) => `${n.host}:${n.port}`));
|
|
2678
|
+
const extras = fetched.filter((n) => !shipped.has(`${n.host}:${n.port}`));
|
|
2679
|
+
const previous = config.carrier.extraBootstrapNodes ?? [];
|
|
2680
|
+
const previousKeys = new Set(previous.map((n) => `${n.host}:${n.port}`));
|
|
2681
|
+
const added = extras.filter((n) => !previousKeys.has(`${n.host}:${n.port}`));
|
|
2682
|
+
const dropped = previous.filter((n) => !extras.some((e) => e.host === n.host && e.port === n.port));
|
|
2683
|
+
console.log(`Feed: ${fetched.length} node(s) — ${fetched.length - extras.length} already shipped in this build, ${extras.length} extra.`);
|
|
2684
|
+
for (const node of extras) {
|
|
2685
|
+
const mark = previousKeys.has(`${node.host}:${node.port}`) ? " " : "+";
|
|
2686
|
+
console.log(` ${mark} ${node.host}:${node.port} ${node.pk}`);
|
|
2687
|
+
}
|
|
2688
|
+
for (const node of dropped) {
|
|
2689
|
+
console.log(` - ${node.host}:${node.port} ${node.pk} (no longer in feed)`);
|
|
2690
|
+
}
|
|
2691
|
+
for (const bad of rejected) {
|
|
2692
|
+
console.log(` ! skipped ${bad}`);
|
|
2693
|
+
}
|
|
2694
|
+
if (args.dryRun) {
|
|
2695
|
+
console.log("\n--dry-run: config not written.");
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (added.length === 0 && dropped.length === 0) {
|
|
2699
|
+
console.log("\nAlready up to date — config unchanged.");
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (extras.length > 0) {
|
|
2703
|
+
config.carrier.extraBootstrapNodes = extras;
|
|
2704
|
+
config.carrier.bootstrapSource = { url, fetchedAt: new Date().toISOString() };
|
|
2705
|
+
}
|
|
2706
|
+
else {
|
|
2707
|
+
delete config.carrier.extraBootstrapNodes;
|
|
2708
|
+
delete config.carrier.bootstrapSource;
|
|
2709
|
+
}
|
|
2710
|
+
config.carrier.bootstrapNodes = mergeBootstrapNodes([...DEFAULT_BOOTSTRAP_NODES], config.carrier.extraBootstrapNodes);
|
|
2711
|
+
await ConfigLoader.save(config, configPath);
|
|
2712
|
+
console.log(`\nWrote ${configPath}: ${added.length} added, ${dropped.length} removed → ${config.carrier.bootstrapNodes.length} bootstrap node(s) total.`);
|
|
2713
|
+
console.log("Takes effect on the next daemon start — 'agentnet restart' (no sudo needed).");
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* `agentnet bootstrap show` — print the list the daemon will actually use,
|
|
2717
|
+
* flagging which entries are shipped defaults vs. adopted from a feed.
|
|
2718
|
+
*
|
|
2719
|
+
* Worth its own command because the `bootstrapNodes:` block sitting in
|
|
2720
|
+
* config.yaml is NOT what gets used — it's overwritten by the shipped defaults
|
|
2721
|
+
* on every load, so reading the file gives the wrong answer.
|
|
2722
|
+
*/
|
|
2723
|
+
export async function cmdBootstrapShow(args) {
|
|
2724
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
2725
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
2726
|
+
const extraKeys = new Set((config.carrier.extraBootstrapNodes ?? []).map((n) => `${n.host}:${n.port}`));
|
|
2727
|
+
console.log(`Effective bootstrap nodes (${config.carrier.bootstrapNodes.length}):`);
|
|
2728
|
+
for (const node of config.carrier.bootstrapNodes) {
|
|
2729
|
+
const origin = extraKeys.has(`${node.host}:${node.port}`) ? "feed" : "shipped";
|
|
2730
|
+
console.log(` ${node.host}:${node.port}`.padEnd(28) + `${node.pk} [${origin}]`);
|
|
2731
|
+
}
|
|
2732
|
+
if (config.carrier.bootstrapSource) {
|
|
2733
|
+
console.log(`\nFeed: ${config.carrier.bootstrapSource.url} (fetched ${config.carrier.bootstrapSource.fetchedAt})`);
|
|
2734
|
+
}
|
|
2735
|
+
else {
|
|
2736
|
+
console.log(`\nNo feed adopted — run 'agentnet bootstrap update' to pull ${DEFAULT_BOOTSTRAP_FEED_URL}.`);
|
|
2737
|
+
}
|
|
2738
|
+
// The live daemon may predate the current config; say so rather than letting
|
|
2739
|
+
// the operator assume the printed list is what's running.
|
|
2740
|
+
const pid = daemonPid(config);
|
|
2741
|
+
if (pid !== null) {
|
|
2742
|
+
const res = await ipcCall(config, { op: "diag" });
|
|
2743
|
+
if (res.ok) {
|
|
2744
|
+
const live = res.data?.dht?.bootstrapsConfigured;
|
|
2745
|
+
if (typeof live === "number" && live !== config.carrier.bootstrapNodes.length) {
|
|
2746
|
+
console.log(`\nNOTE: the running daemon (pid ${pid}) has ${live} bootstrap(s) loaded, not ${config.carrier.bootstrapNodes.length}. Restart it to apply: 'agentnet restart'.`);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hideBin } from "yargs/helpers";
|
|
|
10
10
|
// Belt-and-braces — also raise it here in case the CLI is run directly
|
|
11
11
|
// (e.g. `node dist/cli/index.js` rather than via dist/index.js).
|
|
12
12
|
EventEmitter.defaultMaxListeners = 100;
|
|
13
|
-
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyWhitelist, cmdProxyFileWhitelist, cmdProxyBusyMbps, cmdProxyUse, cmdProxyRouter, cmdProxyTrustCa, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
|
|
13
|
+
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyWhitelist, cmdProxyFileWhitelist, cmdProxyBusyMbps, cmdProxyUse, cmdProxyRouter, cmdProxyTrustCa, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdBootstrapShow, cmdBootstrapUpdate, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
|
|
14
14
|
async function main() {
|
|
15
15
|
await yargs(hideBin(process.argv))
|
|
16
16
|
.scriptName("agentnet")
|
|
@@ -103,6 +103,40 @@ async function main() {
|
|
|
103
103
|
})
|
|
104
104
|
.command("doctor", "Self-check: diagnoses config/daemon/dora/peer problems and prints the fix for each", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
105
105
|
await cmdDoctor({ configDir: argv["config-dir"] });
|
|
106
|
+
})
|
|
107
|
+
// An npm upgrade refreshes the BUILT-IN bootstrap defaults, but only to
|
|
108
|
+
// whatever the release shipped with. When a bootstrap is deployed
|
|
109
|
+
// mid-cycle, this pulls it from the published feed so nodes don't have to
|
|
110
|
+
// wait for the next publish.
|
|
111
|
+
.command("bootstrap", "Show or refresh the Carrier bootstrap node list", (y) => y
|
|
112
|
+
.command("show", "Print the bootstrap nodes the daemon will actually use", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
113
|
+
await cmdBootstrapShow({ configDir: argv["config-dir"] });
|
|
114
|
+
})
|
|
115
|
+
.command("update", "Fetch the published server list and adopt any bootstrap this build doesn't ship", (yy) => yy
|
|
116
|
+
.option("url", {
|
|
117
|
+
type: "string",
|
|
118
|
+
describe: "Feed URL (must be https). Defaults to the published beagle.chat list",
|
|
119
|
+
})
|
|
120
|
+
.option("dry-run", {
|
|
121
|
+
type: "boolean",
|
|
122
|
+
default: false,
|
|
123
|
+
describe: "Show what would change without writing the config",
|
|
124
|
+
})
|
|
125
|
+
.option("clear", {
|
|
126
|
+
type: "boolean",
|
|
127
|
+
default: false,
|
|
128
|
+
describe: "Drop all feed-adopted nodes and go back to the shipped defaults",
|
|
129
|
+
})
|
|
130
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
131
|
+
await cmdBootstrapUpdate({
|
|
132
|
+
url: argv.url,
|
|
133
|
+
dryRun: argv["dry-run"],
|
|
134
|
+
clear: argv.clear,
|
|
135
|
+
configDir: argv["config-dir"],
|
|
136
|
+
});
|
|
137
|
+
})
|
|
138
|
+
.demandCommand(1, "Specify a bootstrap subcommand: show | update"), () => {
|
|
139
|
+
// parent handler — never invoked because demandCommand above
|
|
106
140
|
})
|
|
107
141
|
.command("dns", "Manage OS-side DNS resolver wiring for the .decent zone", (y) => y
|
|
108
142
|
.command("install", "Route .decent queries to the local daemon (writes /etc/resolver on macOS or resolvectl on Linux)", (yy) => yy
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
import type { DecentAgentNetConfig } from "../types.js";
|
|
1
|
+
import type { BootstrapNode, DecentAgentNetConfig } from "../types.js";
|
|
2
|
+
export declare const DEFAULT_BOOTSTRAP_NODES: {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
pk: string;
|
|
6
|
+
}[];
|
|
7
|
+
/**
|
|
8
|
+
* Shipped defaults first, then any node the operator (or `agentnet bootstrap
|
|
9
|
+
* update`) added that we don't already ship, deduped on host:port.
|
|
10
|
+
*
|
|
11
|
+
* Extras are APPENDED, never interleaved: the ordering of
|
|
12
|
+
* DEFAULT_BOOTSTRAP_NODES is deliberate (US relays first so a US↔US session
|
|
13
|
+
* doesn't land on a China relay — see the comment above it), and a remote feed
|
|
14
|
+
* has no idea where this node is. So a fetched list can add reachability, but
|
|
15
|
+
* it can't rearrange the relay-landing preference we tuned.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mergeBootstrapNodes(base: BootstrapNode[], extra?: BootstrapNode[]): BootstrapNode[];
|
|
2
18
|
/**
|
|
3
19
|
* Public dora servers baked into `agentnet init` so an operator can join
|
|
4
20
|
* the canonical Decent AgentNet without first hunting down a registry
|
package/dist/config/loader.js
CHANGED
|
@@ -19,7 +19,7 @@ const DEFAULT_CONFIG_FILE = resolve(DEFAULT_CONFIG_DIR, "config.yaml");
|
|
|
19
19
|
// the SDK's "connect to top-N relays" path naturally picks closer
|
|
20
20
|
// nodes. The China + Singapore nodes stay in the list as fallback for
|
|
21
21
|
// peers actually in those regions.
|
|
22
|
-
const DEFAULT_BOOTSTRAP_NODES = [
|
|
22
|
+
export const DEFAULT_BOOTSTRAP_NODES = [
|
|
23
23
|
// lens (Vultr US) — OUR OWN ela-bootstrapd, same box as the express relay.
|
|
24
24
|
// It was in the iOS app's bootstrap list but missing here, which is a big
|
|
25
25
|
// part of why natives could always bootstrap while JS peers struggled
|
|
@@ -46,6 +46,30 @@ const DEFAULT_BOOTSTRAP_NODES = [
|
|
|
46
46
|
{ host: "52.83.171.135", port: 443, pk: "5tuHgK1Q4CYf4K5PutsEPK5E3Z7cbtEBdx7LwmdzqXHL" },
|
|
47
47
|
{ host: "52.83.191.228", port: 33445, pk: "3khtxZo89SBScAMaHhTvD68pPHiKxgZT6hTCSZZVgNEm" },
|
|
48
48
|
];
|
|
49
|
+
/**
|
|
50
|
+
* Shipped defaults first, then any node the operator (or `agentnet bootstrap
|
|
51
|
+
* update`) added that we don't already ship, deduped on host:port.
|
|
52
|
+
*
|
|
53
|
+
* Extras are APPENDED, never interleaved: the ordering of
|
|
54
|
+
* DEFAULT_BOOTSTRAP_NODES is deliberate (US relays first so a US↔US session
|
|
55
|
+
* doesn't land on a China relay — see the comment above it), and a remote feed
|
|
56
|
+
* has no idea where this node is. So a fetched list can add reachability, but
|
|
57
|
+
* it can't rearrange the relay-landing preference we tuned.
|
|
58
|
+
*/
|
|
59
|
+
export function mergeBootstrapNodes(base, extra) {
|
|
60
|
+
if (!extra?.length)
|
|
61
|
+
return base;
|
|
62
|
+
const seen = new Set(base.map((n) => `${n.host}:${n.port}`));
|
|
63
|
+
const merged = [...base];
|
|
64
|
+
for (const node of extra) {
|
|
65
|
+
const key = `${node.host}:${node.port}`;
|
|
66
|
+
if (seen.has(key))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(key);
|
|
69
|
+
merged.push(node);
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
49
73
|
const DEFAULT_EXPRESS_NODES = [
|
|
50
74
|
// lens stays PRIMARY for backward-compat: older clients (peer < 0.1.25)
|
|
51
75
|
// only know lens and pull from the first relay, so replies must land on
|
|
@@ -282,7 +306,11 @@ export class ConfigLoader {
|
|
|
282
306
|
const dir = dirname(path);
|
|
283
307
|
// Create directory if it doesn't exist
|
|
284
308
|
mkdirSync(dir, { recursive: true });
|
|
285
|
-
|
|
309
|
+
// noRefs: config.yaml is hand-edited. Any object that appears twice in the
|
|
310
|
+
// tree (e.g. a node in both `bootstrapNodes` and `extraBootstrapNodes`)
|
|
311
|
+
// would otherwise be written as a YAML anchor/alias — valid, but it turns
|
|
312
|
+
// the list into `- *ref_0` and silently couples two entries under editing.
|
|
313
|
+
const content = yaml.dump(config, { lineWidth: -1, noRefs: true });
|
|
286
314
|
const fs = await import("fs/promises");
|
|
287
315
|
await fs.writeFile(path, content, "utf-8");
|
|
288
316
|
}
|
|
@@ -304,7 +332,19 @@ export class ConfigLoader {
|
|
|
304
332
|
// pins a US peer to a China relay forever even after we
|
|
305
333
|
// reorder defaults, and every install upgrade keeps the bad
|
|
306
334
|
// first-relay landing pattern.
|
|
307
|
-
|
|
335
|
+
//
|
|
336
|
+
// `extraBootstrapNodes` is the escape hatch: nodes we don't ship yet
|
|
337
|
+
// (published feed, or a private deployment) get APPENDED to the
|
|
338
|
+
// defaults rather than replacing them, so an operator can adopt a
|
|
339
|
+
// freshly-deployed bootstrap without an npm release and still track
|
|
340
|
+
// our ordering. `agentnet bootstrap update` writes that field.
|
|
341
|
+
bootstrapNodes: mergeBootstrapNodes(defaults.carrier.bootstrapNodes, config.carrier?.extraBootstrapNodes),
|
|
342
|
+
...(config.carrier?.extraBootstrapNodes?.length
|
|
343
|
+
? { extraBootstrapNodes: config.carrier.extraBootstrapNodes }
|
|
344
|
+
: {}),
|
|
345
|
+
...(config.carrier?.bootstrapSource
|
|
346
|
+
? { bootstrapSource: config.carrier.bootstrapSource }
|
|
347
|
+
: {}),
|
|
308
348
|
expressNodes: config.carrier?.expressNodes || defaults.carrier.expressNodes,
|
|
309
349
|
},
|
|
310
350
|
network: {
|
package/dist/console/console.js
CHANGED
|
@@ -691,6 +691,18 @@ var DEFAULT_BOOTSTRAP_NODES = [
|
|
|
691
691
|
{ host: "52.83.171.135", port: 443, pk: "5tuHgK1Q4CYf4K5PutsEPK5E3Z7cbtEBdx7LwmdzqXHL" },
|
|
692
692
|
{ host: "52.83.191.228", port: 33445, pk: "3khtxZo89SBScAMaHhTvD68pPHiKxgZT6hTCSZZVgNEm" }
|
|
693
693
|
];
|
|
694
|
+
function mergeBootstrapNodes(base, extra) {
|
|
695
|
+
if (!extra?.length) return base;
|
|
696
|
+
const seen = new Set(base.map((n) => `${n.host}:${n.port}`));
|
|
697
|
+
const merged = [...base];
|
|
698
|
+
for (const node of extra) {
|
|
699
|
+
const key = `${node.host}:${node.port}`;
|
|
700
|
+
if (seen.has(key)) continue;
|
|
701
|
+
seen.add(key);
|
|
702
|
+
merged.push(node);
|
|
703
|
+
}
|
|
704
|
+
return merged;
|
|
705
|
+
}
|
|
694
706
|
var DEFAULT_EXPRESS_NODES = [
|
|
695
707
|
// lens stays PRIMARY for backward-compat: older clients (peer < 0.1.25)
|
|
696
708
|
// only know lens and pull from the first relay, so replies must land on
|
|
@@ -885,7 +897,7 @@ var ConfigLoader = class {
|
|
|
885
897
|
const path = filePath || DEFAULT_CONFIG_FILE;
|
|
886
898
|
const dir = dirname(path);
|
|
887
899
|
mkdirSync(dir, { recursive: true });
|
|
888
|
-
const content = yaml.dump(config, { lineWidth: -1 });
|
|
900
|
+
const content = yaml.dump(config, { lineWidth: -1, noRefs: true });
|
|
889
901
|
const fs = await import("fs/promises");
|
|
890
902
|
await fs.writeFile(path, content, "utf-8");
|
|
891
903
|
}
|
|
@@ -907,7 +919,18 @@ var ConfigLoader = class {
|
|
|
907
919
|
// pins a US peer to a China relay forever even after we
|
|
908
920
|
// reorder defaults, and every install upgrade keeps the bad
|
|
909
921
|
// first-relay landing pattern.
|
|
910
|
-
|
|
922
|
+
//
|
|
923
|
+
// `extraBootstrapNodes` is the escape hatch: nodes we don't ship yet
|
|
924
|
+
// (published feed, or a private deployment) get APPENDED to the
|
|
925
|
+
// defaults rather than replacing them, so an operator can adopt a
|
|
926
|
+
// freshly-deployed bootstrap without an npm release and still track
|
|
927
|
+
// our ordering. `agentnet bootstrap update` writes that field.
|
|
928
|
+
bootstrapNodes: mergeBootstrapNodes(
|
|
929
|
+
defaults.carrier.bootstrapNodes,
|
|
930
|
+
config.carrier?.extraBootstrapNodes
|
|
931
|
+
),
|
|
932
|
+
...config.carrier?.extraBootstrapNodes?.length ? { extraBootstrapNodes: config.carrier.extraBootstrapNodes } : {},
|
|
933
|
+
...config.carrier?.bootstrapSource ? { bootstrapSource: config.carrier.bootstrapSource } : {},
|
|
911
934
|
expressNodes: config.carrier?.expressNodes || defaults.carrier.expressNodes
|
|
912
935
|
},
|
|
913
936
|
network: {
|
package/dist/types.d.ts
CHANGED
|
@@ -119,6 +119,18 @@ export interface CarrierConfig {
|
|
|
119
119
|
dataDir: string;
|
|
120
120
|
bootstrapNodes: BootstrapNode[];
|
|
121
121
|
expressNodes?: BootstrapNode[];
|
|
122
|
+
/** Bootstraps the shipped build doesn't know about, appended to the
|
|
123
|
+
* built-in defaults at load time. `agentnet bootstrap update` fills this
|
|
124
|
+
* from the published server feed so a node can pick up newly-deployed
|
|
125
|
+
* bootstraps without waiting for an npm release. Written by that command;
|
|
126
|
+
* hand-editing is fine too. */
|
|
127
|
+
extraBootstrapNodes?: BootstrapNode[];
|
|
128
|
+
/** Provenance for `extraBootstrapNodes` — which feed it came from and when.
|
|
129
|
+
* Purely informational (printed by `agentnet bootstrap show`). */
|
|
130
|
+
bootstrapSource?: {
|
|
131
|
+
url: string;
|
|
132
|
+
fetchedAt: string;
|
|
133
|
+
};
|
|
122
134
|
}
|
|
123
135
|
export interface NetworkConfig {
|
|
124
136
|
interface: string;
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.251";
|
|
2
2
|
const ICON_PATHS = {
|
|
3
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
4
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.251",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@decentnetwork/dora": "^0.1.14",
|
|
87
|
-
"@decentnetwork/peer": "^0.1.
|
|
87
|
+
"@decentnetwork/peer": "^0.1.123",
|
|
88
88
|
"@decentnetwork/peer-webrtc": "^0.2.10",
|
|
89
89
|
"ink": "^5.2.1",
|
|
90
90
|
"js-yaml": "^4.1.0",
|