@decentnetwork/lan 0.1.112 → 0.1.114
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/console/console.js +6 -1
- package/dist/daemon/message-store.d.ts +14 -2
- package/dist/daemon/message-store.js +12 -1
- package/dist/daemon/server.d.ts +3 -0
- package/dist/daemon/server.js +58 -5
- package/dist/ui/desktop/app.js +9 -3
- package/dist/ui/server.js +7 -1
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/console/console.js
CHANGED
|
@@ -1054,7 +1054,12 @@ var DaemonClient = class _DaemonClient {
|
|
|
1054
1054
|
const arr = chats[userid] ?? [];
|
|
1055
1055
|
return arr.map((m) => {
|
|
1056
1056
|
const file = m.file;
|
|
1057
|
-
|
|
1057
|
+
let suffix = "";
|
|
1058
|
+
if (file?.status === "sending") {
|
|
1059
|
+
const pct = file.size ? Math.min(100, Math.floor((file.sent ?? 0) / file.size * 100)) : 0;
|
|
1060
|
+
suffix = ` \u2014 sending ${pct}%`;
|
|
1061
|
+
} else if (file?.status === "failed") suffix = " \u2014 failed";
|
|
1062
|
+
const text = file ? `\u{1F4CE} ${file.name ?? "file"} (${fileSize(typeof file.size === "number" ? file.size : 0)})${suffix}` : String(m.text ?? "");
|
|
1058
1063
|
return {
|
|
1059
1064
|
t: hhmm(typeof m.ts === "number" ? m.ts : void 0),
|
|
1060
1065
|
who: m.dir === "out" ? "me" : "them",
|
|
@@ -16,10 +16,14 @@ export interface ChatMessage {
|
|
|
16
16
|
id: string;
|
|
17
17
|
/** Present when this entry is a file transfer rather than a text message.
|
|
18
18
|
* `name`/`size` describe the file; received files (dir:"in") are saved to
|
|
19
|
-
* <configDir>/downloads/<name> and downloadable via the UI.
|
|
19
|
+
* <configDir>/downloads/<name> and downloadable via the UI. For outgoing
|
|
20
|
+
* files, `status` tracks delivery and `sent` is the acked byte count (live
|
|
21
|
+
* progress) — the receiver confirms every byte before status becomes "sent". */
|
|
20
22
|
file?: {
|
|
21
23
|
name: string;
|
|
22
24
|
size: number;
|
|
25
|
+
status?: "sending" | "sent" | "failed";
|
|
26
|
+
sent?: number;
|
|
23
27
|
};
|
|
24
28
|
}
|
|
25
29
|
export declare class MessageStore {
|
|
@@ -33,11 +37,19 @@ export declare class MessageStore {
|
|
|
33
37
|
private load;
|
|
34
38
|
/** Append a message and schedule a flush. Returns the stored message. */
|
|
35
39
|
append(peer: string, dir: "in" | "out", text: string, ts?: number): ChatMessage;
|
|
36
|
-
/** Append a file-transfer entry (shown as a
|
|
40
|
+
/** Append a file-transfer entry (shown as a file chip in the UI). */
|
|
37
41
|
appendFile(peer: string, dir: "in" | "out", file: {
|
|
38
42
|
name: string;
|
|
39
43
|
size: number;
|
|
44
|
+
status?: "sending" | "sent" | "failed";
|
|
45
|
+
sent?: number;
|
|
40
46
|
}, ts?: number): ChatMessage;
|
|
47
|
+
/** Patch an existing file message's transfer fields (status / sent bytes).
|
|
48
|
+
* No-op if the id isn't found. Returns true if it patched. */
|
|
49
|
+
patchFile(peer: string, id: string, patch: {
|
|
50
|
+
status?: "sending" | "sent" | "failed";
|
|
51
|
+
sent?: number;
|
|
52
|
+
}): boolean;
|
|
41
53
|
private push;
|
|
42
54
|
/**
|
|
43
55
|
* Return history. With no peer, returns every peer's full thread (the legacy
|
|
@@ -47,10 +47,21 @@ export class MessageStore {
|
|
|
47
47
|
append(peer, dir, text, ts = Date.now()) {
|
|
48
48
|
return this.push(peer, { dir, text, ts, id: `${ts}-${this.seq++}` });
|
|
49
49
|
}
|
|
50
|
-
/** Append a file-transfer entry (shown as a
|
|
50
|
+
/** Append a file-transfer entry (shown as a file chip in the UI). */
|
|
51
51
|
appendFile(peer, dir, file, ts = Date.now()) {
|
|
52
52
|
return this.push(peer, { dir, text: "", ts, id: `${ts}-${this.seq++}`, file });
|
|
53
53
|
}
|
|
54
|
+
/** Patch an existing file message's transfer fields (status / sent bytes).
|
|
55
|
+
* No-op if the id isn't found. Returns true if it patched. */
|
|
56
|
+
patchFile(peer, id, patch) {
|
|
57
|
+
const arr = this.byPeer.get(peer);
|
|
58
|
+
const msg = arr?.find((m) => m.id === id);
|
|
59
|
+
if (!msg || !msg.file)
|
|
60
|
+
return false;
|
|
61
|
+
msg.file = { ...msg.file, ...patch };
|
|
62
|
+
this.scheduleSave();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
54
65
|
push(peer, msg) {
|
|
55
66
|
let arr = this.byPeer.get(peer);
|
|
56
67
|
if (!arr) {
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -41,6 +41,9 @@ export declare class DaemonServer {
|
|
|
41
41
|
* chat, presence changes, and friend-requests so clients can refresh on
|
|
42
42
|
* demand instead of polling on a tight interval. */
|
|
43
43
|
private readonly ipcEvents;
|
|
44
|
+
/** Outgoing file transfers in flight: fileId → the chat message tracking it,
|
|
45
|
+
* so progress/complete/cancel events can patch its status + sent bytes. */
|
|
46
|
+
private readonly activeSends;
|
|
44
47
|
private startedAt;
|
|
45
48
|
private isRunning;
|
|
46
49
|
private statusTimer?;
|
package/dist/daemon/server.js
CHANGED
|
@@ -33,6 +33,21 @@ function shellQuote(parts) {
|
|
|
33
33
|
.map((p) => "'" + p.replace(/'/g, `'\\''`) + "'")
|
|
34
34
|
.join(" ");
|
|
35
35
|
}
|
|
36
|
+
/** Make an incoming filename safe to write to disk AND friendly to open from a
|
|
37
|
+
* shell. Strips path separators + control chars and normalizes exotic Unicode
|
|
38
|
+
* whitespace (e.g. macOS's U+202F narrow no-break space before "PM" in
|
|
39
|
+
* screenshot/recording names) to a plain space, so files don't need shell
|
|
40
|
+
* escaping. Never empty. */
|
|
41
|
+
function sanitizeFileName(name) {
|
|
42
|
+
const cleaned = (name || "file")
|
|
43
|
+
.replace(/[/\\]/g, "_")
|
|
44
|
+
// eslint-disable-next-line no-control-regex
|
|
45
|
+
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
46
|
+
.replace(/[\u00a0\u2000-\u200f\u2028\u2029\u202f\u205f\u3000\ufeff]/g, " ")
|
|
47
|
+
.replace(/\s+/g, " ")
|
|
48
|
+
.trim();
|
|
49
|
+
return cleaned || "file";
|
|
50
|
+
}
|
|
36
51
|
import { ConfigLoader, DEFAULT_DORAS } from "../config/loader.js";
|
|
37
52
|
export class DaemonServer {
|
|
38
53
|
config;
|
|
@@ -61,6 +76,9 @@ export class DaemonServer {
|
|
|
61
76
|
* chat, presence changes, and friend-requests so clients can refresh on
|
|
62
77
|
* demand instead of polling on a tight interval. */
|
|
63
78
|
ipcEvents = new EventEmitter();
|
|
79
|
+
/** Outgoing file transfers in flight: fileId → the chat message tracking it,
|
|
80
|
+
* so progress/complete/cancel events can patch its status + sent bytes. */
|
|
81
|
+
activeSends = new Map();
|
|
64
82
|
startedAt = 0;
|
|
65
83
|
isRunning = false;
|
|
66
84
|
statusTimer;
|
|
@@ -310,8 +328,13 @@ export class DaemonServer {
|
|
|
310
328
|
if (!fileId)
|
|
311
329
|
throw new Error("No free transfer slot (or friend unknown)");
|
|
312
330
|
this.logger.info(`Offering file "${name}" (${data.length}B) to ${userid.slice(0, 8)}`);
|
|
313
|
-
//
|
|
314
|
-
|
|
331
|
+
// Add the "out" chip immediately as STATUS=sending (not "sent" — the
|
|
332
|
+
// transfer is reliable+acked, so it only flips to "sent" once the
|
|
333
|
+
// receiver confirms every byte). Progress/complete/cancel events patch
|
|
334
|
+
// it. The chip is tracked by fileId so those events can find it.
|
|
335
|
+
const msg = this.messageStore?.appendFile(userid, "out", { name: sanitizeFileName(name), size: data.length, status: "sending", sent: 0 });
|
|
336
|
+
if (msg)
|
|
337
|
+
this.activeSends.set(fileId, { peer: userid, msgId: msg.id });
|
|
315
338
|
this.friendMeta?.ensure(userid);
|
|
316
339
|
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
317
340
|
return { fileId, name, size: data.length };
|
|
@@ -588,17 +611,47 @@ export class DaemonServer {
|
|
|
588
611
|
this.logger.info(`Incoming file "${o.name}" (${o.size}B) from ${o.friendId.slice(0, 8)} — accepting`);
|
|
589
612
|
this.peerManager?.acceptFile(o.friendId, o.fileNumber);
|
|
590
613
|
});
|
|
614
|
+
// Live send progress → patch the chip's sent bytes + push so the UI shows
|
|
615
|
+
// "sending …%". Throttled implicitly by the SDK's progress cadence.
|
|
616
|
+
this.peerManager.on("file-progress", (p) => {
|
|
617
|
+
if (!p.sending || !p.fileId)
|
|
618
|
+
return;
|
|
619
|
+
const t = this.activeSends.get(p.fileId);
|
|
620
|
+
if (!t)
|
|
621
|
+
return;
|
|
622
|
+
this.messageStore?.patchFile(t.peer, t.msgId, { sent: p.received });
|
|
623
|
+
this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
|
|
624
|
+
});
|
|
625
|
+
this.peerManager.on("file-cancel", (p) => {
|
|
626
|
+
if (!p.sending)
|
|
627
|
+
return;
|
|
628
|
+
this.logger.warn(`File send to ${p.friendId.slice(0, 8)} aborted (${p.reason ?? "cancelled"})`);
|
|
629
|
+
const t = p.fileId ? this.activeSends.get(p.fileId) : undefined;
|
|
630
|
+
if (t) {
|
|
631
|
+
this.messageStore?.patchFile(t.peer, t.msgId, { status: "failed" });
|
|
632
|
+
this.activeSends.delete(p.fileId);
|
|
633
|
+
this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
|
|
634
|
+
}
|
|
635
|
+
});
|
|
591
636
|
this.peerManager.on("file-complete", (p) => {
|
|
592
637
|
if (p.sending || !p.data) {
|
|
593
|
-
if (p.sending)
|
|
594
|
-
|
|
638
|
+
if (p.sending) {
|
|
639
|
+
// The receiver has ACKed the whole file — now it's truly delivered.
|
|
640
|
+
this.logger.info(`File "${p.name}" delivered to ${p.friendId.slice(0, 8)}`);
|
|
641
|
+
const t = p.fileId ? this.activeSends.get(p.fileId) : undefined;
|
|
642
|
+
if (t) {
|
|
643
|
+
this.messageStore?.patchFile(t.peer, t.msgId, { status: "sent", sent: p.size });
|
|
644
|
+
this.activeSends.delete(p.fileId);
|
|
645
|
+
this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
595
648
|
return;
|
|
596
649
|
}
|
|
597
650
|
void (async () => {
|
|
598
651
|
const dir = resolve(this.configDir, "downloads");
|
|
599
652
|
const fs = await import("fs/promises");
|
|
600
653
|
await fs.mkdir(dir, { recursive: true });
|
|
601
|
-
const safe = (p.name
|
|
654
|
+
const safe = sanitizeFileName(p.name);
|
|
602
655
|
await fs.writeFile(resolve(dir, safe), Buffer.from(p.data));
|
|
603
656
|
this.logger.info(`Saved file "${p.name}" (${p.size}B) from ${p.friendId.slice(0, 8)} → ${resolve(dir, safe)}`);
|
|
604
657
|
// Record in chat history (as a download chip) and push to the UI.
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -267,7 +267,13 @@ function useDaemonData() {
|
|
|
267
267
|
from: m.dir === "out" ? "me" : "them",
|
|
268
268
|
time: dkClock(m.ts),
|
|
269
269
|
text: m.text,
|
|
270
|
-
file: m.file ? {
|
|
270
|
+
file: m.file ? {
|
|
271
|
+
name: m.file.name,
|
|
272
|
+
size: dkFileSize(m.file.size),
|
|
273
|
+
dir: m.dir,
|
|
274
|
+
status: m.file.status,
|
|
275
|
+
pct: m.file.status === "sending" && m.file.size ? Math.min(100, Math.floor((m.file.sent || 0) / m.file.size * 100)) : void 0
|
|
276
|
+
} : void 0,
|
|
271
277
|
status: m.dir === "out" ? "read" : void 0
|
|
272
278
|
}));
|
|
273
279
|
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
@@ -1033,8 +1039,8 @@ function Msg({ m, peer, T }) {
|
|
|
1033
1039
|
}
|
|
1034
1040
|
},
|
|
1035
1041
|
/* @__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.kind || "file", size: 18, stroke: 1.9 })),
|
|
1036
|
-
/* @__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.size
|
|
1037
|
-
mine ? /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
|
|
1042
|
+
/* @__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 === "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 download`)),
|
|
1043
|
+
mine ? 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)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
|
|
1038
1044
|
) : /* @__PURE__ */ React.createElement("div", { style: {
|
|
1039
1045
|
padding: "8px 12px",
|
|
1040
1046
|
borderRadius: 12,
|
package/dist/ui/server.js
CHANGED
|
@@ -307,9 +307,15 @@ export function startFriendUi(opts) {
|
|
|
307
307
|
res.end("not found");
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
|
+
// Content-Disposition must be ASCII — Node throws ERR_INVALID_CHAR on a
|
|
311
|
+
// non-ASCII header value (filenames from macOS contain U+202F before
|
|
312
|
+
// "PM", and others may be Chinese, etc.). Provide an ASCII-only
|
|
313
|
+
// `filename=` fallback plus the RFC 5987 `filename*=UTF-8''…` with the
|
|
314
|
+
// real (percent-encoded) name for clients that support it.
|
|
315
|
+
const asciiName = name.replace(/[^\x20-\x7e]/g, "_").replace(/"/g, "");
|
|
310
316
|
res.writeHead(200, {
|
|
311
317
|
"content-type": "application/octet-stream",
|
|
312
|
-
"content-disposition": `attachment; filename="${
|
|
318
|
+
"content-disposition": `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(name)}`,
|
|
313
319
|
});
|
|
314
320
|
res.end(readFileSync(file));
|
|
315
321
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.114",
|
|
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.50",
|
|
83
83
|
"ink": "^5.2.1",
|
|
84
84
|
"js-yaml": "^4.1.0",
|
|
85
85
|
"react": "^18.3.1",
|