@decentnetwork/lan 0.1.219 → 0.1.221
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/carrier/peer-manager.d.ts +2 -0
- package/dist/carrier/peer-manager.js +4 -0
- package/dist/cli/commands.d.ts +9 -0
- package/dist/cli/commands.js +32 -0
- package/dist/cli/index.js +10 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +24 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/desktop/app.js +38 -10
- package/package.json +1 -1
|
@@ -90,6 +90,8 @@ export declare class PeerManager extends EventEmitter {
|
|
|
90
90
|
isNativeFriend(userid: string): boolean;
|
|
91
91
|
/** Accept an incoming file offer. */
|
|
92
92
|
acceptFile(userid: string, fileNumber: number): void;
|
|
93
|
+
/** Reject/cancel an incoming file offer before accepting it. */
|
|
94
|
+
cancelFile(userid: string, fileNumber: number): void;
|
|
93
95
|
/** Cancel an in-flight send by content fileId. Returns true if it was found. */
|
|
94
96
|
cancelSend(userid: string, fileId: string): boolean;
|
|
95
97
|
/** Cancel in-flight sends matching size/name when activeSends lost the fileId. */
|
|
@@ -205,6 +205,10 @@ export class PeerManager extends EventEmitter {
|
|
|
205
205
|
acceptFile(userid, fileNumber) {
|
|
206
206
|
this.peer?.acceptFile(userid, fileNumber);
|
|
207
207
|
}
|
|
208
|
+
/** Reject/cancel an incoming file offer before accepting it. */
|
|
209
|
+
cancelFile(userid, fileNumber) {
|
|
210
|
+
this.peer?.cancelFile(userid, fileNumber, false);
|
|
211
|
+
}
|
|
208
212
|
/** Cancel an in-flight send by content fileId. Returns true if it was found. */
|
|
209
213
|
cancelSend(userid, fileId) {
|
|
210
214
|
return this.peer?.cancelFileById(userid, fileId, true) ?? false;
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -215,6 +215,15 @@ export declare function cmdProxyWhitelist(args: {
|
|
|
215
215
|
userid?: string;
|
|
216
216
|
configDir?: string;
|
|
217
217
|
}): Promise<void>;
|
|
218
|
+
/**
|
|
219
|
+
* Exit bandwidth guard for Messenger file receiving. Default is OFF: friends can
|
|
220
|
+
* send files normally. When ON, this node only accepts incoming file offers from
|
|
221
|
+
* peers present in the existing proxy whitelist. Applies live to the daemon.
|
|
222
|
+
*/
|
|
223
|
+
export declare function cmdProxyFileWhitelist(args: {
|
|
224
|
+
mode: "on" | "off" | "status";
|
|
225
|
+
configDir?: string;
|
|
226
|
+
}): Promise<void>;
|
|
218
227
|
/**
|
|
219
228
|
* Set (or clear, with 0) the exit's busy threshold in Mbps. Above this smoothed
|
|
220
229
|
* egress rate the exit serves only whitelisted peers; 0 = free for all.
|
package/dist/cli/commands.js
CHANGED
|
@@ -1004,6 +1004,9 @@ export async function cmdProxyWhitelist(args) {
|
|
|
1004
1004
|
console.log(busy > 0
|
|
1005
1005
|
? `Busy threshold: ${busy}Mbps (above this, only whitelisted peers get new tunnels).`
|
|
1006
1006
|
: `Busy threshold: OFF — exit is free for all. Set one with 'agentnet proxy busy-mbps <N>'.`);
|
|
1007
|
+
console.log(proxy.fileWhitelistOnly === true
|
|
1008
|
+
? `File receive guard: ON — incoming files are accepted only from this whitelist.`
|
|
1009
|
+
: `File receive guard: OFF — friend file receiving is open. Enable with 'agentnet proxy file-whitelist on'.`);
|
|
1007
1010
|
return;
|
|
1008
1011
|
}
|
|
1009
1012
|
if (!args.userid) {
|
|
@@ -1025,6 +1028,35 @@ export async function cmdProxyWhitelist(args) {
|
|
|
1025
1028
|
await ConfigLoader.save(config, configPath);
|
|
1026
1029
|
await applyProxyReloadIfRunning(config);
|
|
1027
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Exit bandwidth guard for Messenger file receiving. Default is OFF: friends can
|
|
1033
|
+
* send files normally. When ON, this node only accepts incoming file offers from
|
|
1034
|
+
* peers present in the existing proxy whitelist. Applies live to the daemon.
|
|
1035
|
+
*/
|
|
1036
|
+
export async function cmdProxyFileWhitelist(args) {
|
|
1037
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
1038
|
+
const configPath = resolve(dir, "config.yaml");
|
|
1039
|
+
const config = await ConfigLoader.load(configPath);
|
|
1040
|
+
const proxy = config.proxy ?? { enabled: false, port: 8888 };
|
|
1041
|
+
const whitelist = proxy.whitelist ?? [];
|
|
1042
|
+
const enabled = proxy.fileWhitelistOnly === true;
|
|
1043
|
+
if (args.mode === "status") {
|
|
1044
|
+
console.log(enabled
|
|
1045
|
+
? `File receive guard: ON — incoming files are accepted only from proxy.whitelist (${whitelist.length} entr${whitelist.length === 1 ? "y" : "ies"}).`
|
|
1046
|
+
: `File receive guard: OFF — friend file receiving is open.`);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const next = args.mode === "on";
|
|
1050
|
+
config.proxy = { ...proxy, fileWhitelistOnly: next };
|
|
1051
|
+
await ConfigLoader.save(config, configPath);
|
|
1052
|
+
console.log(next
|
|
1053
|
+
? `File receive guard ON — incoming files from friends outside proxy.whitelist will be rejected.`
|
|
1054
|
+
: `File receive guard OFF — friends can send files normally.`);
|
|
1055
|
+
if (next && whitelist.length === 0) {
|
|
1056
|
+
console.log(`Warning: proxy.whitelist is empty, so all incoming file offers will be rejected until you add peers.`);
|
|
1057
|
+
}
|
|
1058
|
+
await applyProxyReloadIfRunning(config);
|
|
1059
|
+
}
|
|
1028
1060
|
/**
|
|
1029
1061
|
* Set (or clear, with 0) the exit's busy threshold in Mbps. Above this smoothed
|
|
1030
1062
|
* egress rate the exit serves only whitelisted peers; 0 = free for all.
|
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, 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, 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")
|
|
@@ -327,6 +327,15 @@ async function main() {
|
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
329
|
await cmdProxyWhitelist({ action: argv.action, userid: argv.userid, configDir: argv["config-dir"] });
|
|
330
|
+
})
|
|
331
|
+
.command("file-whitelist <mode>", "Incoming files: accept only from proxy whitelist when on (on|off|status)", (yy) => yy
|
|
332
|
+
.positional("mode", { type: "string", demandOption: true })
|
|
333
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
334
|
+
if (argv.mode !== "on" && argv.mode !== "off" && argv.mode !== "status") {
|
|
335
|
+
console.log("Usage: agentnet proxy file-whitelist <on|off|status>");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
await cmdProxyFileWhitelist({ mode: argv.mode, configDir: argv["config-dir"] });
|
|
330
339
|
})
|
|
331
340
|
.command("busy-mbps <mbps>", "Set the exit's busy threshold (Mbps); above it, only whitelisted peers get new tunnels. 0 = free for all", (yy) => yy
|
|
332
341
|
.positional("mbps", { type: "number", demandOption: true })
|
package/dist/daemon/server.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -207,6 +207,20 @@ export class DaemonServer {
|
|
|
207
207
|
}
|
|
208
208
|
this.tunRebuildInProgress = false;
|
|
209
209
|
}
|
|
210
|
+
isFileSenderWhitelisted(userid) {
|
|
211
|
+
const proxy = this.config.proxy;
|
|
212
|
+
if (!proxy?.fileWhitelistOnly)
|
|
213
|
+
return true;
|
|
214
|
+
const whitelist = new Set((proxy.whitelist ?? []).map((s) => String(s).trim()).filter(Boolean));
|
|
215
|
+
if (whitelist.size === 0)
|
|
216
|
+
return false;
|
|
217
|
+
if (whitelist.has(userid))
|
|
218
|
+
return true;
|
|
219
|
+
const rec = this.ipam?.resolveCarrierId(userid);
|
|
220
|
+
if (!rec)
|
|
221
|
+
return false;
|
|
222
|
+
return whitelist.has(rec.virtualIp) || whitelist.has(rec.name) || whitelist.has(rec.carrierId);
|
|
223
|
+
}
|
|
210
224
|
async start() {
|
|
211
225
|
if (this.isRunning) {
|
|
212
226
|
throw new Error("Daemon already running");
|
|
@@ -704,6 +718,7 @@ export class DaemonServer {
|
|
|
704
718
|
// and `proxy busy-mbps` apply live without a restart.
|
|
705
719
|
const whitelist = fresh.proxy?.whitelist ?? [];
|
|
706
720
|
const busyMbps = fresh.proxy?.busyMbps;
|
|
721
|
+
const fileWhitelistOnly = fresh.proxy?.fileWhitelistOnly === true;
|
|
707
722
|
this.connectProxy.updateAdmission({ whitelist, busyMbps });
|
|
708
723
|
// Keep the in-memory config in sync so a later diag reflects it.
|
|
709
724
|
if (this.config.proxy) {
|
|
@@ -712,8 +727,9 @@ export class DaemonServer {
|
|
|
712
727
|
this.config.proxy.allowConnectPorts = allowConnectPorts;
|
|
713
728
|
this.config.proxy.whitelist = whitelist;
|
|
714
729
|
this.config.proxy.busyMbps = busyMbps;
|
|
730
|
+
this.config.proxy.fileWhitelistOnly = fileWhitelistOnly;
|
|
715
731
|
}
|
|
716
|
-
return { applied: true, allowHosts, whitelist, busyMbps };
|
|
732
|
+
return { applied: true, allowHosts, whitelist, busyMbps, fileWhitelistOnly };
|
|
717
733
|
},
|
|
718
734
|
proxyAccess: async (userid, allow) => {
|
|
719
735
|
// Live block/allow a peer's access to THIS node's exit proxy port.
|
|
@@ -1014,6 +1030,13 @@ export class DaemonServer {
|
|
|
1014
1030
|
// File transfer (toxcore-standard): auto-accept offers from friends and
|
|
1015
1031
|
// save completed files under <configDir>/downloads/.
|
|
1016
1032
|
this.peerManager.on("file-offer", (o) => {
|
|
1033
|
+
if (!this.isFileSenderWhitelisted(o.friendId)) {
|
|
1034
|
+
const rec = this.ipam?.resolveCarrierId(o.friendId);
|
|
1035
|
+
const who = rec ? `${rec.name}/${rec.virtualIp}` : o.friendId.slice(0, 8);
|
|
1036
|
+
this.logger.warn(`Incoming file "${o.name}" (${o.size}B) from ${who} rejected — proxy.fileWhitelistOnly=true and sender is not in proxy.whitelist`);
|
|
1037
|
+
this.peerManager?.cancelFile(o.friendId, o.fileNumber);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1017
1040
|
this.logger.info(`Incoming file "${o.name}" (${o.size}B) from ${o.friendId.slice(0, 8)} — accepting`);
|
|
1018
1041
|
this.peerManager?.acceptFile(o.friendId, o.fileNumber);
|
|
1019
1042
|
});
|
package/dist/types.d.ts
CHANGED
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1365,10 +1365,11 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1365
1365
|
), mine && m.status === "queued" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
1366
1366
|
);
|
|
1367
1367
|
}
|
|
1368
|
-
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1368
|
+
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1369
1369
|
const scrollRef = React.useRef(null);
|
|
1370
1370
|
const followScrollRef = React.useRef(true);
|
|
1371
1371
|
const fileRef = React.useRef(null);
|
|
1372
|
+
const rtcFileRef = React.useRef(null);
|
|
1372
1373
|
const [menu, setMenu] = React.useState(false);
|
|
1373
1374
|
const [selMode, setSelMode] = React.useState(false);
|
|
1374
1375
|
const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
|
|
@@ -1541,12 +1542,13 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1541
1542
|
requestAnimationFrame(scrollToBottom);
|
|
1542
1543
|
}
|
|
1543
1544
|
};
|
|
1544
|
-
const sendFiles = (files) => {
|
|
1545
|
-
|
|
1545
|
+
const sendFiles = (files, mode) => {
|
|
1546
|
+
const sender = mode === "webrtc" ? onSendRtcFile : onSendFile;
|
|
1547
|
+
if (!files || !files.length || !sender) return;
|
|
1546
1548
|
const file = files[0];
|
|
1547
1549
|
followScrollRef.current = true;
|
|
1548
|
-
setSending({ name: file.name });
|
|
1549
|
-
Promise.resolve(
|
|
1550
|
+
setSending({ name: file.name, via: mode });
|
|
1551
|
+
Promise.resolve(sender(file)).then((r) => {
|
|
1550
1552
|
if (r && r.ok === false) window.alert((lang === "zh" ? "\u53D1\u9001\u5931\u8D25: " : "Send failed: ") + (r.error || ""));
|
|
1551
1553
|
}).finally(() => {
|
|
1552
1554
|
setSending(null);
|
|
@@ -1572,7 +1574,18 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1572
1574
|
onDrop
|
|
1573
1575
|
},
|
|
1574
1576
|
dragOver && /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 0, zIndex: 30, background: "rgba(91,140,255,0.10)", border: "2px dashed var(--accent)", borderRadius: 12, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", fontFamily: "var(--mono)", fontSize: 14, color: "var(--accent)" } }, lang === "zh" ? "\u677E\u5F00\u53D1\u9001\u6587\u4EF6" : "Drop to send file"),
|
|
1575
|
-
/* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T && T.audioCall || "Audio call", onClick: () => onCall(peer.userId, false) }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "video", title: T && T.videoCall || "Video call", onClick: () => onCall(peer.userId, true) }),
|
|
1577
|
+
/* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T && T.audioCall || "Audio call", onClick: () => onCall(peer.userId, false) }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "video", title: T && T.videoCall || "Video call", onClick: () => onCall(peer.userId, true) }), onSendRtcFile && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
1578
|
+
"input",
|
|
1579
|
+
{
|
|
1580
|
+
ref: rtcFileRef,
|
|
1581
|
+
type: "file",
|
|
1582
|
+
style: { display: "none" },
|
|
1583
|
+
onChange: (e) => {
|
|
1584
|
+
sendFiles(e.target.files, "webrtc");
|
|
1585
|
+
e.target.value = "";
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
), /* @__PURE__ */ React.createElement(Btn, { icon: "file", title: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => rtcFileRef.current && rtcFileRef.current.click() })), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
|
|
1576
1589
|
setMenu(false);
|
|
1577
1590
|
onAlias(peer);
|
|
1578
1591
|
} }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "check", label: T.selectDelete || "Select & delete", onClick: () => {
|
|
@@ -1620,7 +1633,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1620
1633
|
stroke: 2.2,
|
|
1621
1634
|
color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
|
|
1622
1635
|
}
|
|
1623
|
-
), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
|
|
1636
|
+
), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
|
|
1624
1637
|
"input",
|
|
1625
1638
|
{
|
|
1626
1639
|
ref: fileRef,
|
|
@@ -1715,9 +1728,9 @@ function MenuItem({ icon, label, onClick, danger }) {
|
|
|
1715
1728
|
function ChatEmpty({ T }) {
|
|
1716
1729
|
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
|
|
1717
1730
|
}
|
|
1718
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1731
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1719
1732
|
const peer = peers.find((p) => p.id === activeId);
|
|
1720
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1733
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1721
1734
|
}
|
|
1722
1735
|
Object.assign(window, { ChatTab });
|
|
1723
1736
|
function StatTile({ label, value, sub, tone }) {
|
|
@@ -2415,6 +2428,7 @@ const STR = {
|
|
|
2415
2428
|
call: "Voice call",
|
|
2416
2429
|
alias: "Set alias",
|
|
2417
2430
|
sendFile: "Send file",
|
|
2431
|
+
sendWebrtcFile: "Send file via WebRTC",
|
|
2418
2432
|
block: "Block peer",
|
|
2419
2433
|
remove: "Remove friend",
|
|
2420
2434
|
receivedFile: "Received file",
|
|
@@ -2506,6 +2520,7 @@ const STR = {
|
|
|
2506
2520
|
call: "\u8BED\u97F3\u901A\u8BDD",
|
|
2507
2521
|
alias: "\u8BBE\u7F6E\u5907\u6CE8",
|
|
2508
2522
|
sendFile: "\u53D1\u9001\u6587\u4EF6",
|
|
2523
|
+
sendWebrtcFile: "\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6",
|
|
2509
2524
|
block: "\u62C9\u9ED1",
|
|
2510
2525
|
remove: "\u5220\u9664\u597D\u53CB",
|
|
2511
2526
|
receivedFile: "\u6536\u5230\u6587\u4EF6",
|
|
@@ -2661,6 +2676,19 @@ function DkApp() {
|
|
|
2661
2676
|
return r;
|
|
2662
2677
|
});
|
|
2663
2678
|
};
|
|
2679
|
+
const onSendRtcFile = (file) => {
|
|
2680
|
+
if (!activeId || !file) return Promise.resolve({ ok: false, error: "no active peer or file" });
|
|
2681
|
+
return rtcFileCtl.sendFile(activeId, file).then((r) => {
|
|
2682
|
+
if (r && r.ok && r.via === "webrtc") {
|
|
2683
|
+
return dkApi.send(activeId, (t.lang === "zh" ? "\u5DF2\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6: " : "sent file via WebRTC: ") + file.name).then(() => ({ ok: true, via: "webrtc" }));
|
|
2684
|
+
}
|
|
2685
|
+
return { ok: false, error: r && r.error || "WebRTC file send failed" };
|
|
2686
|
+
}).then((r) => {
|
|
2687
|
+
data.loadThread(activeId);
|
|
2688
|
+
data.refresh();
|
|
2689
|
+
return r;
|
|
2690
|
+
});
|
|
2691
|
+
};
|
|
2664
2692
|
const onAlias = (peer) => {
|
|
2665
2693
|
const a = window.prompt("Set alias for this peer (empty to clear):", peer.alias || "");
|
|
2666
2694
|
if (a !== null) dkApi.alias(peer.id, a).then(data.refresh);
|
|
@@ -2678,7 +2706,7 @@ function DkApp() {
|
|
|
2678
2706
|
{ id: "network", icon: "network", label: T.network },
|
|
2679
2707
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
2680
2708
|
];
|
|
2681
|
-
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, onCall, onReloadThread: () => activeId && data.loadThread(activeId) }), 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(
|
|
2709
|
+
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, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId) }), 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(
|
|
2682
2710
|
TweakRadio,
|
|
2683
2711
|
{
|
|
2684
2712
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.221",
|
|
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",
|