@decentnetwork/lan 0.1.136 → 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.
- 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/config/default-doras.yaml +0 -4
- package/config/default-exits.yaml +0 -6
- package/dist/cli/commands.d.ts +32 -0
- package/dist/cli/commands.js +139 -0
- package/dist/cli/index.js +19 -1
- package/dist/config/loader.js +0 -1
- package/dist/console/console.js +0 -2
- package/dist/daemon/ipc.d.ts +12 -1
- package/dist/daemon/ipc.js +9 -0
- package/dist/daemon/message-store.d.ts +22 -6
- package/dist/daemon/message-store.js +30 -3
- package/dist/daemon/server.d.ts +19 -0
- package/dist/daemon/server.js +210 -5
- package/dist/proxy/connect-proxy.d.ts +17 -0
- package/dist/proxy/connect-proxy.js +29 -1
- package/dist/proxy/multi-exit-router.js +130 -1
- package/dist/ui/desktop/app.js +67 -39
- package/dist/ui/desktop/index.html +1 -1
- package/dist/ui/server.js +22 -4
- package/docs/INSTALL.md +20 -6
- package/package.json +2 -2
package/dist/ui/desktop/app.js
CHANGED
|
@@ -285,7 +285,7 @@ function useDaemonData() {
|
|
|
285
285
|
status: m.file.status,
|
|
286
286
|
pct: m.file.status === "sending" && m.file.size ? Math.min(100, Math.floor((m.file.sent || 0) / m.file.size * 100)) : void 0
|
|
287
287
|
} : void 0,
|
|
288
|
-
status: m.dir === "out" ? "read" : void 0
|
|
288
|
+
status: m.dir === "out" ? m.status === "queued" ? "queued" : "read" : void 0
|
|
289
289
|
}));
|
|
290
290
|
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
291
291
|
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
@@ -1032,38 +1032,39 @@ const inputStyle = {
|
|
|
1032
1032
|
};
|
|
1033
1033
|
function Msg({ m, peer, T }) {
|
|
1034
1034
|
const mine = m.from === "me";
|
|
1035
|
-
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0" } }, !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, m.file ?
|
|
1036
|
-
// Received media
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1035
|
+
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0" } }, !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, m.file ? (
|
|
1036
|
+
// Inline preview/player. Received media: as soon as it's saved.
|
|
1037
|
+
// Sent media: once it's fully delivered ('sent') — the daemon keeps a
|
|
1038
|
+
// local copy so it plays on the sender's side too.
|
|
1039
|
+
(m.file.media === "image" || m.file.media === "video" || m.file.media === "audio") && (mine ? m.file.status === "sent" : m.file.status !== "sending" && m.file.status !== "failed" && m.file.status !== "queued") ? (
|
|
1040
|
+
// Received media → inline preview / player, with name, size, download.
|
|
1041
|
+
/* @__PURE__ */ React.createElement("div", { style: {
|
|
1042
|
+
display: "flex",
|
|
1043
|
+
flexDirection: "column",
|
|
1044
|
+
gap: 6,
|
|
1045
|
+
maxWidth: 300,
|
|
1046
|
+
background: "var(--bub-them)",
|
|
1047
|
+
border: "1px solid var(--line)",
|
|
1048
|
+
borderRadius: 12,
|
|
1049
|
+
padding: 6
|
|
1050
|
+
} }, m.file.media === "image" && /* @__PURE__ */ React.createElement("a", { href: dkFileUrl(m.file.name), target: "_blank", rel: "noreferrer", style: { display: "block", lineHeight: 0 } }, /* @__PURE__ */ React.createElement(
|
|
1051
|
+
"img",
|
|
1052
|
+
{
|
|
1053
|
+
src: dkFileUrl(m.file.name),
|
|
1054
|
+
alt: m.file.name,
|
|
1055
|
+
style: { display: "block", maxWidth: "100%", maxHeight: 280, borderRadius: 8, objectFit: "cover" }
|
|
1056
|
+
}
|
|
1057
|
+
)), m.file.media === "video" && /* @__PURE__ */ React.createElement(
|
|
1058
|
+
"video",
|
|
1059
|
+
{
|
|
1060
|
+
src: dkFileUrl(m.file.name),
|
|
1061
|
+
controls: true,
|
|
1062
|
+
preload: "metadata",
|
|
1063
|
+
style: { maxWidth: "100%", maxHeight: 320, borderRadius: 8, background: "#000" }
|
|
1064
|
+
}
|
|
1065
|
+
), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size), /* @__PURE__ */ React.createElement("a", { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" }))))
|
|
1066
|
+
) : (() => {
|
|
1067
|
+
const cardStyle = {
|
|
1067
1068
|
display: "flex",
|
|
1068
1069
|
alignItems: "center",
|
|
1069
1070
|
gap: 10,
|
|
@@ -1074,11 +1075,34 @@ function Msg({ m, peer, T }) {
|
|
|
1074
1075
|
minWidth: 200,
|
|
1075
1076
|
textDecoration: "none",
|
|
1076
1077
|
cursor: mine ? "default" : "pointer"
|
|
1078
|
+
};
|
|
1079
|
+
const icon = /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.media === "image" ? "image" : m.file.media === "video" ? "video" : m.file.media === "audio" ? "play" : "file", size: 18, stroke: 1.9 }));
|
|
1080
|
+
const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 sending ${m.file.pct != null ? m.file.pct + "%" : "\u2026"}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
|
|
1081
|
+
if (mine) {
|
|
1082
|
+
return /* @__PURE__ */ React.createElement("div", { style: cardStyle }, icon, meta, m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)" } }, m.file.pct != null ? m.file.pct + "%" : "\u2026") : m.file.status === "failed" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }));
|
|
1077
1083
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1084
|
+
return /* @__PURE__ */ React.createElement(
|
|
1085
|
+
"div",
|
|
1086
|
+
{
|
|
1087
|
+
style: cardStyle,
|
|
1088
|
+
title: "open",
|
|
1089
|
+
onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
|
|
1090
|
+
},
|
|
1091
|
+
icon,
|
|
1092
|
+
meta,
|
|
1093
|
+
/* @__PURE__ */ React.createElement(
|
|
1094
|
+
"a",
|
|
1095
|
+
{
|
|
1096
|
+
href: dkFileDownloadUrl(m.file.name),
|
|
1097
|
+
download: m.file.name,
|
|
1098
|
+
title: "download",
|
|
1099
|
+
onClick: (e) => e.stopPropagation(),
|
|
1100
|
+
style: { display: "inline-flex", flexShrink: 0 }
|
|
1101
|
+
},
|
|
1102
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
|
|
1103
|
+
)
|
|
1104
|
+
);
|
|
1105
|
+
})()
|
|
1082
1106
|
) : /* @__PURE__ */ React.createElement("div", { style: {
|
|
1083
1107
|
padding: "8px 12px",
|
|
1084
1108
|
borderRadius: 12,
|
|
@@ -1092,7 +1116,7 @@ function Msg({ m, peer, T }) {
|
|
|
1092
1116
|
lineHeight: 1.4,
|
|
1093
1117
|
letterSpacing: -0.1,
|
|
1094
1118
|
wordBreak: "break-word"
|
|
1095
|
-
} }, m.text), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" }))));
|
|
1119
|
+
} }, m.text), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), 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)" }))));
|
|
1096
1120
|
}
|
|
1097
1121
|
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet }) {
|
|
1098
1122
|
const scrollRef = React.useRef(null);
|
|
@@ -1362,6 +1386,8 @@ const STR = {
|
|
|
1362
1386
|
message: "Message",
|
|
1363
1387
|
send: "Send",
|
|
1364
1388
|
pickPeer: "Select a peer to open the conversation",
|
|
1389
|
+
queued: "queued",
|
|
1390
|
+
open: "open",
|
|
1365
1391
|
myIp: "my ip",
|
|
1366
1392
|
copyAddr: "copy address",
|
|
1367
1393
|
peersOnline: "peers online",
|
|
@@ -1437,6 +1463,8 @@ const STR = {
|
|
|
1437
1463
|
message: "\u6D88\u606F",
|
|
1438
1464
|
send: "\u53D1\u9001",
|
|
1439
1465
|
pickPeer: "\u9009\u62E9\u4E00\u4F4D\u597D\u53CB\u5F00\u59CB\u4F1A\u8BDD",
|
|
1466
|
+
queued: "\u5F85\u53D1\u9001",
|
|
1467
|
+
open: "\u6253\u5F00",
|
|
1440
1468
|
myIp: "\u6211\u7684 IP",
|
|
1441
1469
|
copyAddr: "\u590D\u5236\u5730\u5740",
|
|
1442
1470
|
peersOnline: "\u5728\u7EBF\u597D\u53CB",
|
|
@@ -1579,7 +1607,7 @@ function DkApp() {
|
|
|
1579
1607
|
{ id: "network", icon: "network", label: T.network },
|
|
1580
1608
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
1581
1609
|
];
|
|
1582
|
-
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)" } }, "
|
|
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(
|
|
1583
1611
|
TweakRadio,
|
|
1584
1612
|
{
|
|
1585
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>
|
|
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
|
@@ -319,10 +319,28 @@ export function startFriendUi(opts) {
|
|
|
319
319
|
mp3: "audio/mpeg", m4a: "audio/mp4", aac: "audio/aac", wav: "audio/wav",
|
|
320
320
|
ogg: "audio/ogg", flac: "audio/flac", opus: "audio/opus",
|
|
321
321
|
pdf: "application/pdf",
|
|
322
|
+
// Text / markup / code / config → served as UTF-8 text so a plain
|
|
323
|
+
// click opens (previews) them in the browser instead of forcing a
|
|
324
|
+
// download. `.md` and friends are the common case here.
|
|
325
|
+
md: "text/markdown", markdown: "text/markdown",
|
|
326
|
+
txt: "text/plain", text: "text/plain", log: "text/plain",
|
|
327
|
+
json: "application/json", csv: "text/csv", tsv: "text/tab-separated-values",
|
|
328
|
+
xml: "text/xml", yaml: "text/plain", yml: "text/plain",
|
|
329
|
+
ini: "text/plain", conf: "text/plain", cfg: "text/plain", toml: "text/plain",
|
|
330
|
+
html: "text/html", htm: "text/html", css: "text/css",
|
|
331
|
+
js: "text/plain", ts: "text/plain", jsx: "text/plain", tsx: "text/plain",
|
|
332
|
+
py: "text/plain", sh: "text/plain", srt: "text/plain", vtt: "text/vtt",
|
|
322
333
|
};
|
|
323
334
|
const ctype = MIME[ext] || "application/octet-stream";
|
|
324
335
|
const isMedia = ctype.startsWith("image/") || ctype.startsWith("video/") || ctype.startsWith("audio/");
|
|
325
|
-
const
|
|
336
|
+
const isText = ctype.startsWith("text/") || ctype === "application/json" || ctype === "application/pdf";
|
|
337
|
+
// Anything we can render inline (media, text, pdf) opens on a plain GET;
|
|
338
|
+
// `?dl=1` forces a save. Binary stays attachment-only.
|
|
339
|
+
const inlineViewable = isMedia || isText;
|
|
340
|
+
const wantDownload = q.get("dl") === "1" || !inlineViewable;
|
|
341
|
+
// Text needs an explicit charset so the browser renders UTF-8 (Chinese
|
|
342
|
+
// markdown etc.) correctly rather than as mojibake.
|
|
343
|
+
const ctypeHeader = ctype.startsWith("text/") ? `${ctype}; charset=utf-8` : ctype;
|
|
326
344
|
// Content-Disposition must be ASCII — Node throws ERR_INVALID_CHAR on a
|
|
327
345
|
// non-ASCII header value (filenames from macOS contain U+202F before
|
|
328
346
|
// "PM", and others may be Chinese, etc.). Provide an ASCII-only
|
|
@@ -343,7 +361,7 @@ export function startFriendUi(opts) {
|
|
|
343
361
|
return;
|
|
344
362
|
}
|
|
345
363
|
res.writeHead(206, {
|
|
346
|
-
"content-type":
|
|
364
|
+
"content-type": ctypeHeader,
|
|
347
365
|
"content-disposition": disposition,
|
|
348
366
|
"accept-ranges": "bytes",
|
|
349
367
|
"content-range": `bytes ${start}-${end}/${size}`,
|
|
@@ -353,7 +371,7 @@ export function startFriendUi(opts) {
|
|
|
353
371
|
return;
|
|
354
372
|
}
|
|
355
373
|
res.writeHead(200, {
|
|
356
|
-
"content-type":
|
|
374
|
+
"content-type": ctypeHeader,
|
|
357
375
|
"content-disposition": disposition,
|
|
358
376
|
"accept-ranges": "bytes",
|
|
359
377
|
"content-length": String(size),
|
|
@@ -633,7 +651,7 @@ export function startFriendUi(opts) {
|
|
|
633
651
|
const PAGE = `<!doctype html>
|
|
634
652
|
<html lang="en"><head><meta charset="utf-8"/>
|
|
635
653
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
636
|
-
<title>
|
|
654
|
+
<title>Beagle — friends</title>
|
|
637
655
|
<style>
|
|
638
656
|
:root { color-scheme: light dark; }
|
|
639
657
|
body { font: 15px/1.5 -apple-system, system-ui, sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; }
|
package/docs/INSTALL.md
CHANGED
|
@@ -13,13 +13,27 @@ transparently between any two daemons that are Carrier friends.
|
|
|
13
13
|
| | minimum |
|
|
14
14
|
|---|---|
|
|
15
15
|
| Node.js | **20+** |
|
|
16
|
-
| OS | Linux (`x86_64`, `aarch64`)
|
|
16
|
+
| OS | Linux (`x86_64`, `aarch64`), macOS (`aarch64`, `x86_64`), or **Windows 10 / 11 via WSL2** ([Windows guide](INSTALL-WINDOWS.md)) |
|
|
17
17
|
| Root / sudo | Yes — needed to create the TUN device. The daemon drops back to user-mode for everything else. |
|
|
18
18
|
| Outbound network | TCP and UDP to public Carrier bootstrap nodes (ports 33445 + 443). No inbound port needed; no public IP needed. |
|
|
19
19
|
|
|
20
|
-
Windows
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
### Windows
|
|
21
|
+
|
|
22
|
+
Windows **is** supported — but the daemon runs inside **WSL2** (Windows
|
|
23
|
+
Subsystem for Linux), a real Linux environment Windows installs for you, not
|
|
24
|
+
on Windows directly. Requirements:
|
|
25
|
+
|
|
26
|
+
- **Windows 11**, or **Windows 10 version 2004 or later** (build 19041+) —
|
|
27
|
+
this is what the one-command `wsl --install` needs.
|
|
28
|
+
- **Fast (direct UDP) path needs Windows 11, version 22H2 or later.** There you
|
|
29
|
+
turn on WSL **mirrored networking mode**, which lets the direct UDP
|
|
30
|
+
hole-punch succeed (~200 ms — smooth for live video). On **Windows 10**
|
|
31
|
+
mirrored mode doesn't exist, so WSL's NAT blocks inbound UDP and the SDK
|
|
32
|
+
falls back to the TCP relay: it still works, just slower (fine for SSH, chat,
|
|
33
|
+
and most browsing; live sports may stutter).
|
|
34
|
+
|
|
35
|
+
👉 **Non-technical step-by-step for Windows users: [INSTALL-WINDOWS.md](INSTALL-WINDOWS.md)**
|
|
36
|
+
(install WSL → install Node.js → install decentlan, three steps).
|
|
23
37
|
|
|
24
38
|
## Install
|
|
25
39
|
|
|
@@ -52,7 +66,7 @@ Wait ~60–90s, then verify:
|
|
|
52
66
|
|
|
53
67
|
```bash
|
|
54
68
|
agentnet ipam list # should show 6+ peers
|
|
55
|
-
agentnet diag | grep tun # your allocated 10.86.
|
|
69
|
+
agentnet diag | grep tun # your allocated 10.86.x.x
|
|
56
70
|
```
|
|
57
71
|
|
|
58
72
|
> ### If `ipam list` shows only yourself after 2 minutes
|
|
@@ -63,7 +77,7 @@ agentnet diag | grep tun # your allocated 10.86.1.x
|
|
|
63
77
|
>
|
|
64
78
|
> 1. Re-send the friend-request directly through your live daemon:
|
|
65
79
|
> ```bash
|
|
66
|
-
> agentnet friend-request
|
|
80
|
+
> agentnet friend-request NsuN81dZdEoyvwEFgWaHkT8SPJB6UWeRmdYcCGFV5CdbbPXoK2RM
|
|
67
81
|
> ```
|
|
68
82
|
> Then wait 60s and re-check `agentnet ipam list`.
|
|
69
83
|
>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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",
|