@decentnetwork/lan 0.1.137 → 0.1.138

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.
Binary file
Binary file
Binary file
Binary file
@@ -218,6 +218,27 @@ export declare function cmdProxyRevokeHost(args: {
218
218
  export declare function cmdProxyListHosts(args: {
219
219
  configDir?: string;
220
220
  }): Promise<void>;
221
+ /**
222
+ * `agentnet proxy who` — show who is using THIS node's exit right now:
223
+ * per-client active tunnels, total tunnels opened, bytes moved, and refusals.
224
+ * Reads live stats from the running daemon (diag → proxy). Lets an exit
225
+ * operator spot a heavy user before deciding to `agentnet revoke` them.
226
+ */
227
+ export declare function cmdProxyWho(args: {
228
+ configDir?: string;
229
+ }): Promise<void>;
230
+ /**
231
+ * `agentnet proxy block|allow <peer>` — live per-peer exit access control.
232
+ * Under the default "allow" policy, every friend can tunnel through this exit;
233
+ * `block` adds a deny rule on the proxy port (applied to the running daemon
234
+ * instantly, no restart), `allow` removes it. Accepts a userid or a friendly
235
+ * name (resolved via the live roster).
236
+ */
237
+ export declare function cmdProxyAccess(args: {
238
+ peer: string;
239
+ allow: boolean;
240
+ configDir?: string;
241
+ }): Promise<void>;
221
242
  /**
222
243
  * Print the env-var line for a peer to use this node's proxy.
223
244
  */
@@ -1040,6 +1040,102 @@ export async function cmdProxyListHosts(args) {
1040
1040
  for (const h of allowHosts)
1041
1041
  console.log(` ${h}`);
1042
1042
  }
1043
+ /**
1044
+ * `agentnet proxy who` — show who is using THIS node's exit right now:
1045
+ * per-client active tunnels, total tunnels opened, bytes moved, and refusals.
1046
+ * Reads live stats from the running daemon (diag → proxy). Lets an exit
1047
+ * operator spot a heavy user before deciding to `agentnet revoke` them.
1048
+ */
1049
+ export async function cmdProxyWho(args) {
1050
+ const dir = args.configDir || ConfigLoader.defaultConfigDir();
1051
+ const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
1052
+ const pid = daemonPid(config);
1053
+ if (pid === null) {
1054
+ console.error("No running daemon found for this config — start it with 'agentnet up'.");
1055
+ process.exit(1);
1056
+ }
1057
+ const res = await ipcCall(config, { op: "diag" });
1058
+ if (!res.ok)
1059
+ throw new Error(`Daemon diag failed: ${res.error}`);
1060
+ const proxy = res.data?.proxy;
1061
+ if (!proxy) {
1062
+ console.log("The CONNECT proxy isn't running on this node (nobody can use it as an exit).");
1063
+ console.log("Enable it with: agentnet proxy enable");
1064
+ return;
1065
+ }
1066
+ const fmtBytes = (n) => {
1067
+ if (n >= 1024 ** 3)
1068
+ return `${(n / 1024 ** 3).toFixed(2)} GB`;
1069
+ if (n >= 1024 ** 2)
1070
+ return `${(n / 1024 ** 2).toFixed(1)} MB`;
1071
+ if (n >= 1024)
1072
+ return `${(n / 1024).toFixed(0)} KB`;
1073
+ return `${n} B`;
1074
+ };
1075
+ const ago = (ms) => {
1076
+ if (!ms)
1077
+ return "—";
1078
+ const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
1079
+ if (s < 60)
1080
+ return `${s}s ago`;
1081
+ if (s < 3600)
1082
+ return `${Math.floor(s / 60)}m ago`;
1083
+ return `${Math.floor(s / 3600)}h ago`;
1084
+ };
1085
+ console.log(`Exit proxy: ${proxy.active} active tunnel(s), ${proxy.totalOpened} opened total, ` +
1086
+ `${fmtBytes(proxy.bytesTransferred)} moved, ${proxy.totalRefused} refused.`);
1087
+ if (!proxy.peers.length) {
1088
+ console.log("No clients have used this exit yet.");
1089
+ return;
1090
+ }
1091
+ console.log("");
1092
+ console.log(" CLIENT ACTIVE OPENED BYTES REFUSED LAST");
1093
+ for (const p of proxy.peers) {
1094
+ const who = (p.srcName ? `${p.srcName} (${p.src})` : p.src).padEnd(22).slice(0, 22);
1095
+ console.log(` ${who} ${String(p.active).padStart(6)} ${String(p.totalOpened).padStart(6)} ` +
1096
+ `${fmtBytes(p.bytesTransferred).padEnd(11)} ${String(p.totalRefused).padStart(7)} ${ago(p.lastSeenMs)}`);
1097
+ }
1098
+ console.log("");
1099
+ console.log("Cut off a heavy user: agentnet proxy block <userid-or-name>");
1100
+ }
1101
+ /**
1102
+ * `agentnet proxy block|allow <peer>` — live per-peer exit access control.
1103
+ * Under the default "allow" policy, every friend can tunnel through this exit;
1104
+ * `block` adds a deny rule on the proxy port (applied to the running daemon
1105
+ * instantly, no restart), `allow` removes it. Accepts a userid or a friendly
1106
+ * name (resolved via the live roster).
1107
+ */
1108
+ export async function cmdProxyAccess(args) {
1109
+ const dir = args.configDir || ConfigLoader.defaultConfigDir();
1110
+ const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
1111
+ const pid = daemonPid(config);
1112
+ if (pid === null) {
1113
+ console.error("No running daemon found for this config — start it with 'agentnet up'.");
1114
+ process.exit(1);
1115
+ }
1116
+ // Resolve a friendly name to its userid so the deny rule keys on the stable id.
1117
+ let userid = args.peer;
1118
+ try {
1119
+ const { peers } = await fetchLiveIpam(config);
1120
+ const stripDomain = (s) => s.replace(/\.[A-Za-z][A-Za-z0-9-]*$/, "");
1121
+ const hit = peers.find((p) => p.carrierId === args.peer || p.name === args.peer || p.name === stripDomain(args.peer));
1122
+ if (hit?.carrierId)
1123
+ userid = hit.carrierId;
1124
+ }
1125
+ catch {
1126
+ // fall back to the raw arg — the daemon matches on userid OR name
1127
+ }
1128
+ const res = await ipcCall(config, { op: "proxy-access", userid, enabled: args.allow });
1129
+ if (!res.ok)
1130
+ throw new Error(`Daemon refused: ${res.error}`);
1131
+ if (args.allow) {
1132
+ console.log(`✓ ${args.peer} may use this exit again (block removed, live).`);
1133
+ }
1134
+ else {
1135
+ console.log(`✓ ${args.peer} is now BLOCKED from this exit (live — existing tunnels drop on next reconnect).`);
1136
+ console.log(" Re-allow with: agentnet proxy allow " + args.peer);
1137
+ }
1138
+ }
1043
1139
  /**
1044
1140
  * Print the env-var line for a peer to use this node's proxy.
1045
1141
  */
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, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdProxyRouter, 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, cmdProxyUse, cmdProxyRouter, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, 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")
@@ -317,6 +317,19 @@ async function main() {
317
317
  })
318
318
  .command("list-hosts", "List the CONNECT host allowlist", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
319
319
  await cmdProxyListHosts({ configDir: argv["config-dir"] });
320
+ })
321
+ .command("who", "Show who is using THIS node's exit (per-peer tunnels + bytes)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
322
+ await cmdProxyWho({ configDir: argv["config-dir"] });
323
+ })
324
+ .command("block <peer>", "Block a peer from using THIS node's exit (live, no restart)", (yy) => yy
325
+ .positional("peer", { type: "string", demandOption: true })
326
+ .option("config-dir", { type: "string" }), async (argv) => {
327
+ await cmdProxyAccess({ peer: argv.peer, allow: false, configDir: argv["config-dir"] });
328
+ })
329
+ .command("allow <peer>", "Re-allow a previously-blocked peer to use THIS node's exit (live)", (yy) => yy
330
+ .positional("peer", { type: "string", demandOption: true })
331
+ .option("config-dir", { type: "string" }), async (argv) => {
332
+ await cmdProxyAccess({ peer: argv.peer, allow: true, configDir: argv["config-dir"] });
320
333
  })
321
334
  .command("use", "Print env vars to route HTTPS via a peer's proxy node", (yy) => yy
322
335
  .option("peer", { type: "string", demandOption: true, describe: "Peer name or carrier ID" })
@@ -84,6 +84,12 @@ export interface IpcHandlers {
84
84
  * restart (which drops every Carrier session). Returns the applied
85
85
  * allowlist for the CLI to echo. */
86
86
  proxyReload: () => Promise<Record<string, unknown>>;
87
+ /** Block or allow a specific peer's access to THIS node's exit proxy port,
88
+ * LIVE (mutates the running ACL + persists policy.yaml). `allow=true`
89
+ * removes the block; `allow=false` adds a deny rule so the peer can no
90
+ * longer tunnel through this exit. Lets an operator cut off a heavy user
91
+ * without restarting the daemon. */
92
+ proxyAccess: (userid: string, allow: boolean) => Promise<Record<string, unknown>>;
87
93
  /** Re-exec the running daemon with its original argv. The new
88
94
  * process inherits the current process's privileges (so a
89
95
  * root-owned daemon re-execs as root with no sudo prompt),
@@ -94,7 +100,7 @@ export interface IpcHandlers {
94
100
  selfRestart: () => Promise<Record<string, unknown>>;
95
101
  }
96
102
  export interface IpcRequest {
97
- op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "self-restart";
103
+ op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "proxy-access" | "self-restart";
98
104
  address?: string;
99
105
  hello?: string;
100
106
  userid?: string;
@@ -233,6 +233,10 @@ export class IpcServer {
233
233
  }
234
234
  case "proxy-reload":
235
235
  return await this.handlers.proxyReload();
236
+ case "proxy-access":
237
+ if (!req.userid)
238
+ throw new Error("proxy-access requires a userid");
239
+ return await this.handlers.proxyAccess(req.userid, req.enabled !== false);
236
240
  case "self-restart":
237
241
  return await this.handlers.selfRestart();
238
242
  default:
@@ -507,6 +507,33 @@ export class DaemonServer {
507
507
  }
508
508
  return { applied: true, allowHosts };
509
509
  },
510
+ proxyAccess: async (userid, allow) => {
511
+ // Live block/allow a peer's access to THIS node's exit proxy port.
512
+ // Mutates the running ACL (evaluated per-packet, so it's instant) and
513
+ // persists policy.yaml. Under the default "allow" policy, a friend can
514
+ // use the exit unless we add an explicit DENY — so "block" adds a deny
515
+ // rule on the proxy port and "allow" removes the peer's rules.
516
+ if (!this.policy)
517
+ throw new Error("ACL policy not initialised");
518
+ const port = this.config.proxy?.port ?? 8888;
519
+ // Accept a userid or a friendly name; store the rule under whatever
520
+ // identifier the ACL matches on (both are checked at evaluate time).
521
+ const peerKey = userid;
522
+ if (allow) {
523
+ this.policy.removePeer(peerKey);
524
+ }
525
+ else {
526
+ this.policy.addRule({
527
+ peer: peerKey,
528
+ direction: "both",
529
+ deny: [{ proto: "tcp", port, purpose: "exit-block" }],
530
+ audit: true,
531
+ });
532
+ }
533
+ await this.policy.save();
534
+ this.logger.info(`Exit access ${allow ? "ALLOWED" : "BLOCKED"} for ${peerKey.slice(0, 12)} (tcp:${port})`);
535
+ return { applied: true, userid: peerKey, allowed: allow, port };
536
+ },
510
537
  selfRestart: async () => {
511
538
  // Detach a child process that waits briefly and then re-execs
512
539
  // the same argv we were started with. Inherits privileges
@@ -586,6 +613,10 @@ export class DaemonServer {
586
613
  session: this.peerManager?.getSessionStatus(f.pubkey) ?? null,
587
614
  })),
588
615
  ipam: ipamPeers,
616
+ // Exit-operator view: live per-client usage of THIS node's CONNECT
617
+ // proxy (who's tunnelling through me + how much). Null when the
618
+ // proxy isn't running.
619
+ proxy: this.connectProxy?.getStats() ?? null,
589
620
  };
590
621
  },
591
622
  });
@@ -51,6 +51,17 @@ export interface ConnectProxyOptions {
51
51
  bytesTransferred: number;
52
52
  }) => void;
53
53
  }
54
+ /** Per-client (per source virtual-IP) usage of the exit, so the operator can
55
+ * see WHO is using their exit and HOW MUCH — spot heavy/abusive users. */
56
+ export interface ProxyPeerUsage {
57
+ src: string;
58
+ srcName?: string;
59
+ active: number;
60
+ totalOpened: number;
61
+ totalRefused: number;
62
+ bytesTransferred: number;
63
+ lastSeenMs: number;
64
+ }
54
65
  export interface ConnectProxyStats {
55
66
  bindIp: string;
56
67
  port: number;
@@ -59,13 +70,19 @@ export interface ConnectProxyStats {
59
70
  totalOpened: number;
60
71
  totalRefused: number;
61
72
  bytesTransferred: number;
73
+ /** Per-client breakdown, heaviest (by bytes) first. */
74
+ peers: ProxyPeerUsage[];
62
75
  }
63
76
  export declare class ConnectProxy {
64
77
  private opts;
65
78
  private server;
66
79
  private logger;
67
80
  private stats;
81
+ /** Live per-client usage, keyed by source virtual IP. */
82
+ private perPeer;
68
83
  constructor(opts: ConnectProxyOptions);
84
+ /** Get-or-create the per-client usage row for a source IP. */
85
+ private peerRow;
69
86
  start(): Promise<void>;
70
87
  stop(): Promise<void>;
71
88
  getStats(): ConnectProxyStats;
@@ -27,6 +27,8 @@ export class ConnectProxy {
27
27
  server = null;
28
28
  logger;
29
29
  stats;
30
+ /** Live per-client usage, keyed by source virtual IP. */
31
+ perPeer = new Map();
30
32
  constructor(opts) {
31
33
  this.opts = opts;
32
34
  this.logger = new Logger({ prefix: "ConnectProxy" });
@@ -38,8 +40,20 @@ export class ConnectProxy {
38
40
  totalOpened: 0,
39
41
  totalRefused: 0,
40
42
  bytesTransferred: 0,
43
+ peers: [],
41
44
  };
42
45
  }
46
+ /** Get-or-create the per-client usage row for a source IP. */
47
+ peerRow(src, srcName) {
48
+ let row = this.perPeer.get(src);
49
+ if (!row) {
50
+ row = { src, srcName, active: 0, totalOpened: 0, totalRefused: 0, bytesTransferred: 0, lastSeenMs: 0 };
51
+ this.perPeer.set(src, row);
52
+ }
53
+ if (srcName && !row.srcName)
54
+ row.srcName = srcName;
55
+ return row;
56
+ }
43
57
  async start() {
44
58
  if (this.server) {
45
59
  throw new Error("ConnectProxy already started");
@@ -84,7 +98,10 @@ export class ConnectProxy {
84
98
  this.logger.info("Stopped");
85
99
  }
86
100
  getStats() {
87
- return { ...this.stats };
101
+ const peers = [...this.perPeer.values()]
102
+ .sort((a, b) => b.bytesTransferred - a.bytesTransferred || b.totalOpened - a.totalOpened)
103
+ .map((p) => ({ ...p }));
104
+ return { ...this.stats, peers };
88
105
  }
89
106
  /**
90
107
  * Update the host allowlist (and optionally the port allowlist) of a
@@ -139,6 +156,10 @@ export class ConnectProxy {
139
156
  clientSocket.destroy();
140
157
  this.stats.active = Math.max(0, this.stats.active - 1);
141
158
  this.stats.bytesTransferred += bytes;
159
+ const row = this.peerRow(src, srcName);
160
+ row.active = Math.max(0, row.active - 1);
161
+ row.bytesTransferred += bytes;
162
+ row.lastSeenMs = Date.now();
142
163
  this.opts.onTunnelClose?.({
143
164
  src,
144
165
  srcName,
@@ -161,6 +182,10 @@ export class ConnectProxy {
161
182
  }
162
183
  this.stats.active += 1;
163
184
  this.stats.totalOpened += 1;
185
+ const row = this.peerRow(src, srcName);
186
+ row.active += 1;
187
+ row.totalOpened += 1;
188
+ row.lastSeenMs = Date.now();
164
189
  this.opts.onTunnelOpen?.({ src, srcName, target: `${host}:${port}` });
165
190
  this.logger.info(`tunnel open ${src}${srcName ? ` (${srcName})` : ""} -> ${host}:${port}`);
166
191
  });
@@ -208,6 +233,9 @@ export class ConnectProxy {
208
233
  }
209
234
  refuse(clientSocket, src, srcName, target, code, reason) {
210
235
  this.stats.totalRefused += 1;
236
+ const row = this.peerRow(src, srcName);
237
+ row.totalRefused += 1;
238
+ row.lastSeenMs = Date.now();
211
239
  this.logger.info(`tunnel refused ${src}${srcName ? ` (${srcName})` : ""} -> ${target}: ${reason}`);
212
240
  try {
213
241
  const statusText = code === 400 ? "Bad Request" : "Forbidden";
@@ -1607,7 +1607,7 @@ function DkApp() {
1607
1607
  { id: "network", icon: "network", label: T.network },
1608
1608
  { id: "profile", icon: "userRound", label: T.profile }
1609
1609
  ];
1610
- return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "decentlan"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
1610
+ return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
1611
1611
  TweakRadio,
1612
1612
  {
1613
1613
  label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>decentlan — Beagle Chat</title>
6
+ <title>Beagle</title>
7
7
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
8
8
  <style>
9
9
  * { box-sizing: border-box; }
package/dist/ui/server.js CHANGED
@@ -651,7 +651,7 @@ export function startFriendUi(opts) {
651
651
  const PAGE = `<!doctype html>
652
652
  <html lang="en"><head><meta charset="utf-8"/>
653
653
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
654
- <title>decentlan — friends</title>
654
+ <title>Beagle — friends</title>
655
655
  <style>
656
656
  :root { color-scheme: light dark; }
657
657
  body { font: 15px/1.5 -apple-system, system-ui, sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.137",
3
+ "version": "0.1.138",
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",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@decentnetwork/dora": "^0.1.11",
82
- "@decentnetwork/peer": "^0.1.70",
82
+ "@decentnetwork/peer": "^0.1.71",
83
83
  "ink": "^5.2.1",
84
84
  "js-yaml": "^4.1.0",
85
85
  "react": "^18.3.1",