@decentnetwork/lan 0.1.0 → 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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/cli/commands.d.ts +37 -4
- package/dist/cli/commands.js +130 -12
- package/dist/cli/index.js +30 -6
- package/dist/daemon/ipc.d.ts +12 -1
- package/dist/daemon/ipc.js +14 -0
- package/dist/daemon/pending-friends.d.ts +52 -0
- package/dist/daemon/pending-friends.js +80 -0
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +68 -16
- package/docs/INSTALL.md +10 -7
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -104,6 +104,31 @@ export declare function cmdFriendAccept(args: {
|
|
|
104
104
|
export declare function cmdFriendsList(args: {
|
|
105
105
|
configDir?: string;
|
|
106
106
|
}): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* List queued friend-requests held by the running daemon (the ones
|
|
109
|
+
* received while `friends.autoAccept` was false). Goes over IPC —
|
|
110
|
+
* the daemon must be up.
|
|
111
|
+
*/
|
|
112
|
+
export declare function cmdFriendsPending(args: {
|
|
113
|
+
configDir?: string;
|
|
114
|
+
}): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Accept a queued friend-request by userid. Routes through IPC;
|
|
117
|
+
* the daemon's own Carrier peer does the actual acceptFriendRequest
|
|
118
|
+
* call and drops the entry from disk. No daemon shutdown needed.
|
|
119
|
+
*/
|
|
120
|
+
export declare function cmdFriendsAccept(args: {
|
|
121
|
+
userid: string;
|
|
122
|
+
configDir?: string;
|
|
123
|
+
}): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Drop a queued friend-request without accepting. Sender doesn't
|
|
126
|
+
* get a notification — they just see no acceptance.
|
|
127
|
+
*/
|
|
128
|
+
export declare function cmdFriendsReject(args: {
|
|
129
|
+
userid: string;
|
|
130
|
+
configDir?: string;
|
|
131
|
+
}): Promise<void>;
|
|
107
132
|
/**
|
|
108
133
|
* Show audit log
|
|
109
134
|
*/
|
|
@@ -198,12 +223,20 @@ export declare function cmdDiag(args: {
|
|
|
198
223
|
configDir?: string;
|
|
199
224
|
}): Promise<void>;
|
|
200
225
|
/**
|
|
201
|
-
* Enable dora integration
|
|
202
|
-
*
|
|
203
|
-
*
|
|
226
|
+
* Enable dora integration. Accepts either `--address` (preferred — the
|
|
227
|
+
* full Carrier address of the dora server; we derive its userid and
|
|
228
|
+
* also send the one-time friend-request automatically) or `--userid`
|
|
229
|
+
* (legacy — expects the operator to have already friended the server).
|
|
230
|
+
*
|
|
231
|
+
* The one-shot `--address` path is the right UX: an operator setting
|
|
232
|
+
* up a fresh node only knows "use this dora server, here's its
|
|
233
|
+
* address" — they shouldn't have to know that dora is also a Carrier
|
|
234
|
+
* peer, that they need to friend it first, or what the difference
|
|
235
|
+
* between userid and address is.
|
|
204
236
|
*/
|
|
205
237
|
export declare function cmdDoraEnable(args: {
|
|
206
|
-
|
|
238
|
+
address?: string;
|
|
239
|
+
userid?: string;
|
|
207
240
|
configDir?: string;
|
|
208
241
|
}): Promise<void>;
|
|
209
242
|
/**
|
package/dist/cli/commands.js
CHANGED
|
@@ -405,8 +405,15 @@ export async function cmdFriendRequest(args) {
|
|
|
405
405
|
const waitMs = args.waitMs ?? 30000;
|
|
406
406
|
console.log(`Waiting ${waitMs}ms for relay delivery...`);
|
|
407
407
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
408
|
+
const myPubkey = peer.pubkey();
|
|
409
|
+
const myUserid = peer.userid();
|
|
408
410
|
await peer.stop();
|
|
409
|
-
console.log(`Friend request sent
|
|
411
|
+
console.log(`Friend request sent.`);
|
|
412
|
+
console.log(`If the recipient's daemon is running with friends.autoAccept (the default), it has already accepted —`);
|
|
413
|
+
console.log(`no action needed on their side. They can confirm with 'agentnet diag' and look for userid ${myUserid}.`);
|
|
414
|
+
console.log(`If their daemon was offline, the request is queued via the express relay; their daemon will`);
|
|
415
|
+
console.log(`pick it up on next start (and still auto-accept). Only run 'agentnet friend-accept --pubkey ${myPubkey}'`);
|
|
416
|
+
console.log(`manually if their daemon is down AND they've disabled autoAccept.`);
|
|
410
417
|
}
|
|
411
418
|
/**
|
|
412
419
|
* Accept a pending friend request.
|
|
@@ -500,10 +507,10 @@ export async function cmdFriendsList(args) {
|
|
|
500
507
|
for (const friend of friends) {
|
|
501
508
|
console.log(``);
|
|
502
509
|
console.log(` ${friend.name || "(unnamed)"} status=${friend.status}`);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
510
|
+
// userid is Carrier's first-class identifier; show that, hide
|
|
511
|
+
// the raw hex pubkey unless someone explicitly needs it.
|
|
512
|
+
const userid = friend.userid && friend.userid !== friend.pubkey ? friend.userid : friend.pubkey;
|
|
513
|
+
console.log(` userid: ${userid}`);
|
|
507
514
|
if (friend.address)
|
|
508
515
|
console.log(` address: ${friend.address}`);
|
|
509
516
|
if (friend.acceptedAt)
|
|
@@ -511,6 +518,69 @@ export async function cmdFriendsList(args) {
|
|
|
511
518
|
}
|
|
512
519
|
await peer.stop();
|
|
513
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* List queued friend-requests held by the running daemon (the ones
|
|
523
|
+
* received while `friends.autoAccept` was false). Goes over IPC —
|
|
524
|
+
* the daemon must be up.
|
|
525
|
+
*/
|
|
526
|
+
export async function cmdFriendsPending(args) {
|
|
527
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
528
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
529
|
+
if (daemonPid(config) === null) {
|
|
530
|
+
throw new Error("Daemon not running — pending friend-requests are held by the daemon. Start it with 'agentnet up' first.");
|
|
531
|
+
}
|
|
532
|
+
const res = await ipcCall(config, { op: "friends-pending" });
|
|
533
|
+
if (!res.ok)
|
|
534
|
+
throw new Error(`Daemon refused: ${res.error}`);
|
|
535
|
+
const list = res.data?.pending ?? [];
|
|
536
|
+
if (list.length === 0) {
|
|
537
|
+
console.log("No pending friend-requests.");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
console.log(`Pending friend-requests (${list.length}):`);
|
|
541
|
+
for (const e of list) {
|
|
542
|
+
console.log("");
|
|
543
|
+
console.log(` ${e.name || "(unnamed)"}`);
|
|
544
|
+
console.log(` userid: ${e.userid}`);
|
|
545
|
+
if (e.hello)
|
|
546
|
+
console.log(` hello: ${e.hello}`);
|
|
547
|
+
console.log(` arrived: ${e.arrivedAt}`);
|
|
548
|
+
}
|
|
549
|
+
console.log("");
|
|
550
|
+
console.log("Accept with: agentnet friends accept --userid <USERID>");
|
|
551
|
+
console.log("Reject with: agentnet friends reject --userid <USERID>");
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Accept a queued friend-request by userid. Routes through IPC;
|
|
555
|
+
* the daemon's own Carrier peer does the actual acceptFriendRequest
|
|
556
|
+
* call and drops the entry from disk. No daemon shutdown needed.
|
|
557
|
+
*/
|
|
558
|
+
export async function cmdFriendsAccept(args) {
|
|
559
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
560
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
561
|
+
if (daemonPid(config) === null) {
|
|
562
|
+
throw new Error("Daemon not running. Start it with 'agentnet up' first — pending friend-requests are managed by the live daemon.");
|
|
563
|
+
}
|
|
564
|
+
const res = await ipcCall(config, { op: "friends-accept", userid: args.userid });
|
|
565
|
+
if (!res.ok)
|
|
566
|
+
throw new Error(`Daemon refused: ${res.error}`);
|
|
567
|
+
console.log(`Accepted ${args.userid}.`);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Drop a queued friend-request without accepting. Sender doesn't
|
|
571
|
+
* get a notification — they just see no acceptance.
|
|
572
|
+
*/
|
|
573
|
+
export async function cmdFriendsReject(args) {
|
|
574
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
575
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
576
|
+
if (daemonPid(config) === null) {
|
|
577
|
+
throw new Error("Daemon not running. Start it with 'agentnet up' first.");
|
|
578
|
+
}
|
|
579
|
+
const res = await ipcCall(config, { op: "friends-reject", userid: args.userid });
|
|
580
|
+
if (!res.ok)
|
|
581
|
+
throw new Error(`Daemon refused: ${res.error}`);
|
|
582
|
+
console.log(`Rejected ${args.userid}.`);
|
|
583
|
+
}
|
|
514
584
|
/**
|
|
515
585
|
* Show audit log
|
|
516
586
|
*/
|
|
@@ -861,27 +931,75 @@ export async function cmdDiag(args) {
|
|
|
861
931
|
console.log(JSON.stringify(res.data, null, 2));
|
|
862
932
|
}
|
|
863
933
|
/**
|
|
864
|
-
* Enable dora integration
|
|
865
|
-
*
|
|
866
|
-
*
|
|
934
|
+
* Enable dora integration. Accepts either `--address` (preferred — the
|
|
935
|
+
* full Carrier address of the dora server; we derive its userid and
|
|
936
|
+
* also send the one-time friend-request automatically) or `--userid`
|
|
937
|
+
* (legacy — expects the operator to have already friended the server).
|
|
938
|
+
*
|
|
939
|
+
* The one-shot `--address` path is the right UX: an operator setting
|
|
940
|
+
* up a fresh node only knows "use this dora server, here's its
|
|
941
|
+
* address" — they shouldn't have to know that dora is also a Carrier
|
|
942
|
+
* peer, that they need to friend it first, or what the difference
|
|
943
|
+
* between userid and address is.
|
|
867
944
|
*/
|
|
868
945
|
export async function cmdDoraEnable(args) {
|
|
946
|
+
if (!args.address && !args.userid) {
|
|
947
|
+
throw new Error("Need one of --address (preferred) or --userid");
|
|
948
|
+
}
|
|
949
|
+
// Derive userid from address when given an address. Address format:
|
|
950
|
+
// base58(pubkey || nospam || checksum); userid: base58(pubkey).
|
|
951
|
+
let userid = args.userid;
|
|
952
|
+
if (args.address) {
|
|
953
|
+
const { carrierIdFromAddress } = await import("@decentnetwork/peer");
|
|
954
|
+
try {
|
|
955
|
+
userid = carrierIdFromAddress(args.address);
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
throw new Error(`--address didn't parse as a Carrier address: ${err instanceof Error ? err.message : err}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
869
961
|
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
870
962
|
const configPath = resolve(dir, "config.yaml");
|
|
871
963
|
const config = await ConfigLoader.load(configPath);
|
|
964
|
+
// Send the friend-request first when we have an address. We route
|
|
965
|
+
// it via the daemon's IPC if it's running, or fall back to a
|
|
966
|
+
// standalone short-lived Peer when it isn't (same logic as
|
|
967
|
+
// cmdFriendRequest).
|
|
968
|
+
if (args.address) {
|
|
969
|
+
console.log(`Friending dora server at ${args.address.slice(0, 24)}...`);
|
|
970
|
+
try {
|
|
971
|
+
await cmdFriendRequest({
|
|
972
|
+
address: args.address,
|
|
973
|
+
hello: "decentlan: enable dora",
|
|
974
|
+
waitMs: 8000,
|
|
975
|
+
configDir: args.configDir,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
// If the SDK already knows this address as an accepted friend
|
|
980
|
+
// it short-circuits — treat that as success, not failure.
|
|
981
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
982
|
+
if (!/already (accepted|a friend|friend)/i.test(msg)) {
|
|
983
|
+
console.warn(`(friend-request had a warning: ${msg})`);
|
|
984
|
+
console.warn(`Proceeding with config update anyway — re-run 'agentnet friend-request' later if you see "friend offline" in the daemon log.`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
872
988
|
const existing = config.dora ?? { enabled: false, userids: [], refreshIntervalMs: 60_000 };
|
|
873
989
|
const userids = new Set(existing.userids ?? []);
|
|
874
|
-
userids.add(
|
|
990
|
+
userids.add(userid);
|
|
875
991
|
config.dora = {
|
|
876
992
|
enabled: true,
|
|
877
993
|
userids: [...userids],
|
|
878
994
|
refreshIntervalMs: existing.refreshIntervalMs ?? 60_000,
|
|
879
995
|
};
|
|
880
996
|
await ConfigLoader.save(config, configPath);
|
|
881
|
-
console.log(`Dora enabled. Server userid added: ${
|
|
997
|
+
console.log(`Dora enabled. Server userid added: ${userid}`);
|
|
882
998
|
console.log(`Total dora servers configured: ${config.dora.userids?.length}`);
|
|
883
|
-
|
|
884
|
-
|
|
999
|
+
if (!args.address) {
|
|
1000
|
+
console.log(`Heads-up: passed --userid only; if you haven't friended the dora server, run 'agentnet friend-request --address <ADDRESS>' before starting the daemon.`);
|
|
1001
|
+
}
|
|
1002
|
+
console.log(`Restart the daemon (or just 'agentnet up') to take effect.`);
|
|
885
1003
|
}
|
|
886
1004
|
/**
|
|
887
1005
|
* Disable dora integration without losing the configured server list.
|
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, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDiag, cmdDnsInstall, cmdDnsHosts, } from "./commands.js";
|
|
13
|
+
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDiag, cmdDnsInstall, cmdDnsHosts, } from "./commands.js";
|
|
14
14
|
async function main() {
|
|
15
15
|
await yargs(hideBin(process.argv))
|
|
16
16
|
.scriptName("agentnet")
|
|
@@ -126,8 +126,21 @@ async function main() {
|
|
|
126
126
|
configDir: argv["config-dir"],
|
|
127
127
|
});
|
|
128
128
|
})
|
|
129
|
-
.command("friends list", "List Carrier friends", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
129
|
+
.command("friends list", "List Carrier friends (daemon must be down)", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
130
130
|
await cmdFriendsList({ configDir: argv["config-dir"] });
|
|
131
|
+
})
|
|
132
|
+
.command("friends pending", "List queued friend-requests (over IPC; daemon must be up)", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
133
|
+
await cmdFriendsPending({ configDir: argv["config-dir"] });
|
|
134
|
+
})
|
|
135
|
+
.command("friends accept", "Accept a queued friend-request by userid (over IPC; daemon stays up)", (y) => y
|
|
136
|
+
.option("userid", { type: "string", demandOption: true, describe: "Sender's Carrier userid (base58)" })
|
|
137
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
138
|
+
await cmdFriendsAccept({ userid: argv.userid, configDir: argv["config-dir"] });
|
|
139
|
+
})
|
|
140
|
+
.command("friends reject", "Drop a queued friend-request by userid (over IPC; daemon stays up)", (y) => y
|
|
141
|
+
.option("userid", { type: "string", demandOption: true })
|
|
142
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
143
|
+
await cmdFriendsReject({ userid: argv.userid, configDir: argv["config-dir"] });
|
|
131
144
|
})
|
|
132
145
|
.command("audit log", "View audit log", (y) => y.option("tail", { type: "number", default: 50 }).option("config-dir", { type: "string" }), async (argv) => {
|
|
133
146
|
await cmdAuditLog({ tail: argv.tail, configDir: argv["config-dir"] });
|
|
@@ -170,10 +183,21 @@ async function main() {
|
|
|
170
183
|
// parent handler — never invoked because demandCommand above
|
|
171
184
|
})
|
|
172
185
|
.command("dora", "Manage the dora (DHCP-style) registry integration (run 'agentnet dora --help')", (y) => y
|
|
173
|
-
.command("enable", "Enable dora
|
|
174
|
-
.option("
|
|
175
|
-
.option("
|
|
176
|
-
|
|
186
|
+
.command("enable", "Enable dora — preferred: --address <addr> (we friend + derive userid); legacy: --userid <id>", (yy) => yy
|
|
187
|
+
.option("address", { type: "string", describe: "Dora server's full Carrier address — friends it and derives the userid for you" })
|
|
188
|
+
.option("userid", { type: "string", describe: "Legacy: server's userid; only use if you've already friended it separately" })
|
|
189
|
+
.option("config-dir", { type: "string" })
|
|
190
|
+
.check((argv) => {
|
|
191
|
+
if (!argv.address && !argv.userid) {
|
|
192
|
+
throw new Error("Need one of --address (preferred) or --userid");
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}), async (argv) => {
|
|
196
|
+
await cmdDoraEnable({
|
|
197
|
+
address: argv.address,
|
|
198
|
+
userid: argv.userid,
|
|
199
|
+
configDir: argv["config-dir"],
|
|
200
|
+
});
|
|
177
201
|
})
|
|
178
202
|
.command("disable", "Disable dora integration (keep configured server list)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
179
203
|
await cmdDoraDisable({ configDir: argv["config-dir"] });
|
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -29,11 +29,22 @@ export interface IpcHandlers {
|
|
|
29
29
|
* Used by `agentnet diag` to inspect a live daemon without
|
|
30
30
|
* restarting it with AGENTNET_LOG_LEVEL=debug. */
|
|
31
31
|
diag: () => Promise<Record<string, unknown>>;
|
|
32
|
+
/** List queued friend-requests that arrived while autoAccept was
|
|
33
|
+
* off. Returns an array of {userid, name, hello, arrivedAt}. */
|
|
34
|
+
friendsPending: () => Promise<Record<string, unknown>>;
|
|
35
|
+
/** Accept a queued friend-request by userid. Drops it from the
|
|
36
|
+
* queue and calls peer.acceptFriendRequest. Idempotent — no-op
|
|
37
|
+
* if userid isn't in the queue. */
|
|
38
|
+
friendsAccept: (userid: string) => Promise<void>;
|
|
39
|
+
/** Reject (drop) a queued friend-request by userid. Doesn't
|
|
40
|
+
* notify the sender — they'll just see no acceptance. */
|
|
41
|
+
friendsReject: (userid: string) => Promise<void>;
|
|
32
42
|
}
|
|
33
43
|
export interface IpcRequest {
|
|
34
|
-
op: "friend-request" | "ping" | "diag";
|
|
44
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject";
|
|
35
45
|
address?: string;
|
|
36
46
|
hello?: string;
|
|
47
|
+
userid?: string;
|
|
37
48
|
}
|
|
38
49
|
export interface IpcResponseOk {
|
|
39
50
|
ok: true;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -130,6 +130,20 @@ export class IpcServer {
|
|
|
130
130
|
}
|
|
131
131
|
case "diag":
|
|
132
132
|
return await this.handlers.diag();
|
|
133
|
+
case "friends-pending":
|
|
134
|
+
return await this.handlers.friendsPending();
|
|
135
|
+
case "friends-accept": {
|
|
136
|
+
if (!req.userid)
|
|
137
|
+
throw new Error("userid is required");
|
|
138
|
+
await this.handlers.friendsAccept(req.userid);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
case "friends-reject": {
|
|
142
|
+
if (!req.userid)
|
|
143
|
+
throw new Error("userid is required");
|
|
144
|
+
await this.handlers.friendsReject(req.userid);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
133
147
|
default:
|
|
134
148
|
throw new Error(`unknown op: ${req.op}`);
|
|
135
149
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending-friend-request store.
|
|
3
|
+
*
|
|
4
|
+
* When `friends.autoAccept` is false (or for any other reason the
|
|
5
|
+
* operator wants to gate inbound friend requests), the daemon must
|
|
6
|
+
* NOT block the running session waiting for a CLI dance. It buffers
|
|
7
|
+
* each incoming request to disk and exposes IPC ops so an operator
|
|
8
|
+
* can list / accept / reject them on their own time, without
|
|
9
|
+
* stopping the daemon. Lost-on-restart pending requests would be
|
|
10
|
+
* frustrating, so the queue is persisted.
|
|
11
|
+
*
|
|
12
|
+
* File format: a single JSON array under
|
|
13
|
+
* `<configDir>/pending-friends.json`. Each entry is the same shape
|
|
14
|
+
* the SDK emits in the friend-request event, plus an arrivedAt
|
|
15
|
+
* timestamp.
|
|
16
|
+
*/
|
|
17
|
+
export interface PendingFriendEntry {
|
|
18
|
+
/** Carrier userid (base58, same 32 pubkey bytes as the hex form
|
|
19
|
+
* the SDK emits — we store the userid form because that's what
|
|
20
|
+
* every user-facing surface uses). */
|
|
21
|
+
userid: string;
|
|
22
|
+
/** Hex pubkey, kept for compatibility with SDK calls that expect
|
|
23
|
+
* that form (acceptFriendRequest takes the pubkey/userid string
|
|
24
|
+
* the original event carried). */
|
|
25
|
+
pubkey: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
hello?: string;
|
|
28
|
+
/** ISO 8601 timestamp; helps operators triage queues of pending
|
|
29
|
+
* requests. */
|
|
30
|
+
arrivedAt: string;
|
|
31
|
+
}
|
|
32
|
+
export declare class PendingFriendsStore {
|
|
33
|
+
private filePath;
|
|
34
|
+
private logger;
|
|
35
|
+
private entries;
|
|
36
|
+
constructor(filePath: string);
|
|
37
|
+
private load;
|
|
38
|
+
private save;
|
|
39
|
+
list(): PendingFriendEntry[];
|
|
40
|
+
/**
|
|
41
|
+
* Add (or replace) a pending request. The same userid arriving
|
|
42
|
+
* twice replaces the older entry — operators see the latest
|
|
43
|
+
* `hello` / `name` / `arrivedAt`, never a stale duplicate.
|
|
44
|
+
*/
|
|
45
|
+
add(entry: PendingFriendEntry): void;
|
|
46
|
+
/**
|
|
47
|
+
* Drop an entry by userid. Returns the removed entry (so the
|
|
48
|
+
* caller has the original pubkey to pass to the SDK's
|
|
49
|
+
* acceptFriendRequest), or null if not present.
|
|
50
|
+
*/
|
|
51
|
+
remove(userid: string): PendingFriendEntry | null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending-friend-request store.
|
|
3
|
+
*
|
|
4
|
+
* When `friends.autoAccept` is false (or for any other reason the
|
|
5
|
+
* operator wants to gate inbound friend requests), the daemon must
|
|
6
|
+
* NOT block the running session waiting for a CLI dance. It buffers
|
|
7
|
+
* each incoming request to disk and exposes IPC ops so an operator
|
|
8
|
+
* can list / accept / reject them on their own time, without
|
|
9
|
+
* stopping the daemon. Lost-on-restart pending requests would be
|
|
10
|
+
* frustrating, so the queue is persisted.
|
|
11
|
+
*
|
|
12
|
+
* File format: a single JSON array under
|
|
13
|
+
* `<configDir>/pending-friends.json`. Each entry is the same shape
|
|
14
|
+
* the SDK emits in the friend-request event, plus an arrivedAt
|
|
15
|
+
* timestamp.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
18
|
+
import { dirname } from "path";
|
|
19
|
+
import { Logger } from "../utils/logger.js";
|
|
20
|
+
export class PendingFriendsStore {
|
|
21
|
+
filePath;
|
|
22
|
+
logger;
|
|
23
|
+
entries = [];
|
|
24
|
+
constructor(filePath) {
|
|
25
|
+
this.filePath = filePath;
|
|
26
|
+
this.logger = new Logger({ prefix: "PendingFriends" });
|
|
27
|
+
this.load();
|
|
28
|
+
}
|
|
29
|
+
load() {
|
|
30
|
+
if (!existsSync(this.filePath))
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (Array.isArray(parsed)) {
|
|
36
|
+
this.entries = parsed.filter((e) => typeof e === "object" &&
|
|
37
|
+
e !== null &&
|
|
38
|
+
typeof e.userid === "string");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
this.logger.warn(`Could not parse ${this.filePath}: ${err}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
save() {
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
48
|
+
writeFileSync(this.filePath, JSON.stringify(this.entries, null, 2), "utf-8");
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
this.logger.warn(`Could not write ${this.filePath}: ${err}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
list() {
|
|
55
|
+
return [...this.entries];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Add (or replace) a pending request. The same userid arriving
|
|
59
|
+
* twice replaces the older entry — operators see the latest
|
|
60
|
+
* `hello` / `name` / `arrivedAt`, never a stale duplicate.
|
|
61
|
+
*/
|
|
62
|
+
add(entry) {
|
|
63
|
+
this.entries = this.entries.filter((e) => e.userid !== entry.userid);
|
|
64
|
+
this.entries.push(entry);
|
|
65
|
+
this.save();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Drop an entry by userid. Returns the removed entry (so the
|
|
69
|
+
* caller has the original pubkey to pass to the SDK's
|
|
70
|
+
* acceptFriendRequest), or null if not present.
|
|
71
|
+
*/
|
|
72
|
+
remove(userid) {
|
|
73
|
+
const idx = this.entries.findIndex((e) => e.userid === userid);
|
|
74
|
+
if (idx < 0)
|
|
75
|
+
return null;
|
|
76
|
+
const [removed] = this.entries.splice(idx, 1);
|
|
77
|
+
this.save();
|
|
78
|
+
return removed;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/daemon/server.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { ConnectProxy } from "../proxy/connect-proxy.js";
|
|
|
16
16
|
import { DoraIntegration } from "../dora/dora-integration.js";
|
|
17
17
|
import { DnsServer } from "../dns/server.js";
|
|
18
18
|
import { IpcServer, ipcSocketPath } from "./ipc.js";
|
|
19
|
+
import { PendingFriendsStore } from "./pending-friends.js";
|
|
19
20
|
import { Logger } from "../utils/logger.js";
|
|
20
21
|
export class DaemonServer {
|
|
21
22
|
config;
|
|
@@ -35,6 +36,7 @@ export class DaemonServer {
|
|
|
35
36
|
doraIntegration;
|
|
36
37
|
ipcServer;
|
|
37
38
|
dnsServer;
|
|
39
|
+
pendingFriends;
|
|
38
40
|
startedAt = 0;
|
|
39
41
|
isRunning = false;
|
|
40
42
|
pidFile;
|
|
@@ -107,6 +109,31 @@ export class DaemonServer {
|
|
|
107
109
|
friendRequest: async (address, hello) => {
|
|
108
110
|
await this.peerManager.sendFriendRequest(address, hello);
|
|
109
111
|
},
|
|
112
|
+
friendsPending: async () => {
|
|
113
|
+
const entries = this.pendingFriends?.list() ?? [];
|
|
114
|
+
// Hide the legacy hex `pubkey` field from the wire — the
|
|
115
|
+
// CLI side only needs userid + metadata to display + accept.
|
|
116
|
+
return {
|
|
117
|
+
pending: entries.map((e) => ({
|
|
118
|
+
userid: e.userid,
|
|
119
|
+
name: e.name,
|
|
120
|
+
hello: e.hello,
|
|
121
|
+
arrivedAt: e.arrivedAt,
|
|
122
|
+
})),
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
friendsAccept: async (userid) => {
|
|
126
|
+
const entry = this.pendingFriends?.remove(userid);
|
|
127
|
+
if (!entry)
|
|
128
|
+
throw new Error(`No pending friend-request for userid ${userid}`);
|
|
129
|
+
await this.peerManager.acceptFriendRequest(entry.pubkey);
|
|
130
|
+
},
|
|
131
|
+
friendsReject: async (userid) => {
|
|
132
|
+
const entry = this.pendingFriends?.remove(userid);
|
|
133
|
+
if (!entry)
|
|
134
|
+
throw new Error(`No pending friend-request for userid ${userid}`);
|
|
135
|
+
// No-op at the Carrier level — sender just sees no acceptance.
|
|
136
|
+
},
|
|
110
137
|
diag: async () => {
|
|
111
138
|
// Snapshot of everything an operator needs to debug why
|
|
112
139
|
// packets aren't moving: forwarding counters, friend
|
|
@@ -188,24 +215,49 @@ export class DaemonServer {
|
|
|
188
215
|
});
|
|
189
216
|
}
|
|
190
217
|
// Live friend-request handling. The daemon stays up and accepts
|
|
191
|
-
// (or
|
|
192
|
-
// ceremony where the operator has to stop the daemon. Default
|
|
193
|
-
// auto-accept; the Carrier friend store IS the trust
|
|
194
|
-
// requests only reach a peer whose address was
|
|
218
|
+
// (or queues) every incoming request — no `friend-accept --wait`
|
|
219
|
+
// ceremony where the operator has to stop the daemon. Default
|
|
220
|
+
// is auto-accept; the Carrier friend store IS the trust
|
|
221
|
+
// boundary, and requests only reach a peer whose address was
|
|
222
|
+
// deliberately shared.
|
|
223
|
+
//
|
|
224
|
+
// When autoAccept is off, the request is persisted to
|
|
225
|
+
// pending-friends.json on disk. The operator inspects/handles
|
|
226
|
+
// it later via the IPC-routed CLI:
|
|
227
|
+
// agentnet friends pending
|
|
228
|
+
// agentnet friends accept --userid <userid>
|
|
229
|
+
// agentnet friends reject --userid <userid>
|
|
230
|
+
// Pending requests survive daemon restarts.
|
|
195
231
|
const autoAcceptFriends = this.config.friends?.autoAccept ?? true;
|
|
232
|
+
const pendingPath = resolve(this.config.carrier.dataDir, "..", "pending-friends.json");
|
|
233
|
+
this.pendingFriends = new PendingFriendsStore(pendingPath);
|
|
234
|
+
const pubkeyHexToUserid = async (pubkeyHex) => {
|
|
235
|
+
const { carrierIdFromPublicKey } = await import("@decentnetwork/peer");
|
|
236
|
+
const bytes = Buffer.from(pubkeyHex, "hex");
|
|
237
|
+
return carrierIdFromPublicKey(new Uint8Array(bytes));
|
|
238
|
+
};
|
|
196
239
|
this.peerManager.on("friend-request", (req) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
`(friends.autoAccept=false
|
|
208
|
-
|
|
240
|
+
void pubkeyHexToUserid(req.pubkey).then((userid) => {
|
|
241
|
+
const who = `${req.name || "(unnamed)"} ${userid}`;
|
|
242
|
+
const hello = req.hello ? ` hello="${req.hello.slice(0, 60)}"` : "";
|
|
243
|
+
if (autoAcceptFriends) {
|
|
244
|
+
this.logger.info(`Friend request from ${who}${hello} — auto-accepting`);
|
|
245
|
+
this.peerManager?.acceptFriendRequest(req.pubkey).catch((err) => {
|
|
246
|
+
this.logger.warn(`Auto-accept failed for ${who}: ${err}`);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
this.logger.info(`Friend request from ${who}${hello} — queued (friends.autoAccept=false). ` +
|
|
251
|
+
`Run 'agentnet friends pending' to triage.`);
|
|
252
|
+
this.pendingFriends?.add({
|
|
253
|
+
userid,
|
|
254
|
+
pubkey: req.pubkey,
|
|
255
|
+
name: req.name,
|
|
256
|
+
hello: req.hello,
|
|
257
|
+
arrivedAt: new Date().toISOString(),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
209
261
|
});
|
|
210
262
|
// NOTE: auto-IPAM (hash-derived virtual IP per userid) was tried and
|
|
211
263
|
// reverted — it produced IPs like 10.86.175.35 that conflicted with
|
package/docs/INSTALL.md
CHANGED
|
@@ -41,17 +41,20 @@ agentnet --help
|
|
|
41
41
|
# 1. Generate this machine's Carrier identity + default config
|
|
42
42
|
agentnet init --name my-laptop
|
|
43
43
|
|
|
44
|
-
# 2.
|
|
45
|
-
#
|
|
46
|
-
|
|
44
|
+
# 2. Point at the dora name server. Pass the FULL ADDRESS (whoever
|
|
45
|
+
# runs dora publishes it). This one command friends dora,
|
|
46
|
+
# derives its userid, and writes the config — no separate
|
|
47
|
+
# friend-request step needed.
|
|
48
|
+
agentnet dora enable --address <dora's Carrier address>
|
|
47
49
|
|
|
48
|
-
# 3.
|
|
49
|
-
agentnet dora enable --userid 98rsHv17h8G6AP9RagyrBiT1kmw4cn8MFPEembS6ZVjv
|
|
50
|
-
|
|
51
|
-
# 4. Start the daemon (needs sudo for the TUN device)
|
|
50
|
+
# 3. Start the daemon (needs sudo for the TUN device)
|
|
52
51
|
sudo $(which agentnet) up --real-tun
|
|
53
52
|
```
|
|
54
53
|
|
|
54
|
+
Three commands, no "userid vs address" gymnastics. The daemon will
|
|
55
|
+
register with dora, pull the roster, and auto-friend every other
|
|
56
|
+
peer in the network.
|
|
57
|
+
|
|
55
58
|
The daemon will:
|
|
56
59
|
|
|
57
60
|
1. Open a Carrier connection
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|