@decentnetwork/lan 0.1.155 → 0.1.157
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/carrier/peer-manager.d.ts +8 -0
- package/dist/carrier/peer-manager.js +19 -1
- package/dist/daemon/message-store.d.ts +6 -1
- package/dist/daemon/message-store.js +3 -1
- package/dist/daemon/server.js +37 -4
- package/dist/ui/desktop/app.js +17 -4
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -62,6 +62,14 @@ export declare class PeerManager extends EventEmitter {
|
|
|
62
62
|
sign(message: Uint8Array): Uint8Array;
|
|
63
63
|
/** Offer a file to a friend (toxcore-standard transfer). Returns the fileId. */
|
|
64
64
|
sendFile(userid: string, data: Uint8Array, name: string): string | null;
|
|
65
|
+
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
66
|
+
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
67
|
+
* receive online. Use for native friends; JS friends keep sendFile(). */
|
|
68
|
+
sendInlineFile(userid: string, data: Uint8Array, name: string): Promise<void>;
|
|
69
|
+
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
70
|
+
* relay/DHT key differs from its identity key. JS peers announce their
|
|
71
|
+
* identity key as their DHT key, so the two match. */
|
|
72
|
+
isNativeFriend(userid: string): boolean;
|
|
65
73
|
/** Accept an incoming file offer. */
|
|
66
74
|
acceptFile(userid: string, fileNumber: number): void;
|
|
67
75
|
/** Cancel an in-flight send by content fileId. Returns true if it was found. */
|
|
@@ -121,6 +121,21 @@ export class PeerManager extends EventEmitter {
|
|
|
121
121
|
throw new Error("Peer not created. Call create() first.");
|
|
122
122
|
return this.peer.sendFile(userid, data, { name });
|
|
123
123
|
}
|
|
124
|
+
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
125
|
+
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
126
|
+
* receive online. Use for native friends; JS friends keep sendFile(). */
|
|
127
|
+
async sendInlineFile(userid, data, name) {
|
|
128
|
+
if (!this.peer)
|
|
129
|
+
throw new Error("Peer not created. Call create() first.");
|
|
130
|
+
await this.peer.sendInlineFile(userid, { name, data });
|
|
131
|
+
}
|
|
132
|
+
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
133
|
+
* relay/DHT key differs from its identity key. JS peers announce their
|
|
134
|
+
* identity key as their DHT key, so the two match. */
|
|
135
|
+
isNativeFriend(userid) {
|
|
136
|
+
const rec = this.peer?.friends().find((f) => f.pubkey === userid);
|
|
137
|
+
return !!(rec?.dhtPubkey && rec.dhtPubkey !== rec.pubkey);
|
|
138
|
+
}
|
|
124
139
|
/** Accept an incoming file offer. */
|
|
125
140
|
acceptFile(userid, fileNumber) {
|
|
126
141
|
this.peer?.acceptFile(userid, fileNumber);
|
|
@@ -514,8 +529,11 @@ export class PeerManager extends EventEmitter {
|
|
|
514
529
|
if (message.text.length === 0) {
|
|
515
530
|
return;
|
|
516
531
|
}
|
|
517
|
-
this.emit("message", message.pubkey, message.text);
|
|
532
|
+
this.emit("message", message.pubkey, message.text, message.via);
|
|
518
533
|
});
|
|
534
|
+
// Inline files (iOS/C Carrier style: FileModel JSON over bulkmsg).
|
|
535
|
+
// The SDK reassembles + decodes; forward to the daemon to save + log.
|
|
536
|
+
this.peer.onInlineFile((f) => this.emit("inline-file", f));
|
|
519
537
|
// Toxcore-standard file transfer (native-compatible). Forward offers,
|
|
520
538
|
// progress, and completions to the daemon, which auto-accepts and saves.
|
|
521
539
|
this.peer.onFile((o) => this.emit("file-offer", o));
|
|
@@ -18,6 +18,11 @@ export interface ChatMessage {
|
|
|
18
18
|
* user hit send, so the daemon stored it and will deliver it (in order) the
|
|
19
19
|
* moment the friend reconnects. Cleared once actually sent. */
|
|
20
20
|
status?: "queued" | "sent" | "failed";
|
|
21
|
+
/** How this message traversed the network, for the UI to distinguish at a
|
|
22
|
+
* glance (different colors). "online" = live net_crypto session (direct/relay);
|
|
23
|
+
* "offline" = express store-and-forward (the friend or we were offline). Lets
|
|
24
|
+
* a user SEE when online delivery is silently failing and only offline lands. */
|
|
25
|
+
via?: "online" | "offline";
|
|
21
26
|
/** Present when this entry is a file transfer rather than a text message.
|
|
22
27
|
* `name`/`size` describe the file; received files (dir:"in") are saved to
|
|
23
28
|
* <configDir>/downloads/<name> and downloadable via the UI. For outgoing
|
|
@@ -47,7 +52,7 @@ export declare class MessageStore {
|
|
|
47
52
|
/** Append a message and schedule a flush. Returns the stored message.
|
|
48
53
|
* Pass `status: "queued"` for an outgoing text the peer wasn't online to
|
|
49
54
|
* receive — the daemon flushes it on reconnect. */
|
|
50
|
-
append(peer: string, dir: "in" | "out", text: string, ts?: number, status?: "queued" | "sent" | "failed"): ChatMessage;
|
|
55
|
+
append(peer: string, dir: "in" | "out", text: string, ts?: number, status?: "queued" | "sent" | "failed", via?: "online" | "offline"): ChatMessage;
|
|
51
56
|
/** Set (or, with undefined, clear) the delivery status on a text message.
|
|
52
57
|
* Used to flip a "queued" message to delivered once it's actually sent.
|
|
53
58
|
* No-op if the id isn't found or is a file entry. Returns true if patched. */
|
|
@@ -46,10 +46,12 @@ export class MessageStore {
|
|
|
46
46
|
/** Append a message and schedule a flush. Returns the stored message.
|
|
47
47
|
* Pass `status: "queued"` for an outgoing text the peer wasn't online to
|
|
48
48
|
* receive — the daemon flushes it on reconnect. */
|
|
49
|
-
append(peer, dir, text, ts = Date.now(), status) {
|
|
49
|
+
append(peer, dir, text, ts = Date.now(), status, via) {
|
|
50
50
|
const msg = { dir, text, ts, id: `${ts}-${this.seq++}` };
|
|
51
51
|
if (status)
|
|
52
52
|
msg.status = status;
|
|
53
|
+
if (via)
|
|
54
|
+
msg.via = via;
|
|
53
55
|
return this.push(peer, msg);
|
|
54
56
|
}
|
|
55
57
|
/** Set (or, with undefined, clear) the delivery status on a text message.
|
package/dist/daemon/server.js
CHANGED
|
@@ -455,6 +455,23 @@ export class DaemonServer {
|
|
|
455
455
|
this.logger.info(`Queued file "${name}" (${data.length}B) for offline ${userid.slice(0, 8)} (delivers on reconnect)`);
|
|
456
456
|
return { queued: true, name: safe, size: data.length };
|
|
457
457
|
}
|
|
458
|
+
// NATIVE friends (iOS/Android/C Carrier) cannot see the toxcore
|
|
459
|
+
// file-transfer protocol at all — the only online file path they
|
|
460
|
+
// receive is the inline FileModel-JSON-over-bulkmsg envelope the
|
|
461
|
+
// Beagle apps themselves use. Detected via the DHT-key signature.
|
|
462
|
+
if (this.peerManager?.isNativeFriend(userid)) {
|
|
463
|
+
const INLINE_MAX = 2 * 1024 * 1024; // base64 inflates ~1.37x; protocol cap is 5MB
|
|
464
|
+
if (data.length > INLINE_MAX) {
|
|
465
|
+
throw new Error(`This friend is a native (iOS/Android) client — files are sent inline and limited to ` +
|
|
466
|
+
`${(INLINE_MAX / 1024 / 1024) | 0} MB (this is ${(data.length / 1024 / 1024).toFixed(1)} MB).`);
|
|
467
|
+
}
|
|
468
|
+
await this.peerManager.sendInlineFile(userid, new Uint8Array(data), name);
|
|
469
|
+
this.logger.info(`Sent inline file "${name}" (${data.length}B) to native peer ${userid.slice(0, 8)}`);
|
|
470
|
+
this.messageStore?.appendFile(userid, "out", { name: sanitizeFileName(name), size: data.length, status: "sent", sent: data.length });
|
|
471
|
+
this.friendMeta?.ensure(userid);
|
|
472
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
473
|
+
return { inline: true, name, size: data.length };
|
|
474
|
+
}
|
|
458
475
|
const fileId = this.peerManager?.sendFile(userid, new Uint8Array(data), name);
|
|
459
476
|
if (!fileId)
|
|
460
477
|
throw new Error("No free transfer slot (or friend unknown)");
|
|
@@ -806,8 +823,8 @@ export class DaemonServer {
|
|
|
806
823
|
return input;
|
|
807
824
|
};
|
|
808
825
|
// Chat: log incoming Carrier text messages (packet 64) for the UI.
|
|
809
|
-
this.peerManager.on("message", (pubkey, text) => {
|
|
810
|
-
this.logChat(pubkey, "in", text);
|
|
826
|
+
this.peerManager.on("message", (pubkey, text, via) => {
|
|
827
|
+
this.logChat(pubkey, "in", text, via);
|
|
811
828
|
});
|
|
812
829
|
// Presence: push to IPC subscribers so the TUI/UI reflects online/offline
|
|
813
830
|
// transitions immediately instead of on the next poll tick.
|
|
@@ -898,6 +915,22 @@ export class DaemonServer {
|
|
|
898
915
|
this.ipcEvents.emit("event", { type: "chat", userid: p.friendId, dir: "in" });
|
|
899
916
|
})().catch((e) => this.logger.warn(`Failed to save file: ${e.message}`));
|
|
900
917
|
});
|
|
918
|
+
// Inline files (iOS/C Carrier style — FileModel JSON over bulkmsg,
|
|
919
|
+
// reassembled + decoded by the SDK). Save like a completed transfer so
|
|
920
|
+
// the chat shows a media chip instead of a wall of base64 "text".
|
|
921
|
+
this.peerManager.on("inline-file", (f) => {
|
|
922
|
+
void (async () => {
|
|
923
|
+
const dir = resolve(this.configDir, "downloads");
|
|
924
|
+
const fs = await import("fs/promises");
|
|
925
|
+
await fs.mkdir(dir, { recursive: true });
|
|
926
|
+
const safe = sanitizeFileName(f.name);
|
|
927
|
+
await fs.writeFile(resolve(dir, safe), Buffer.from(f.data));
|
|
928
|
+
this.logger.info(`Saved inline file "${f.name}" (${f.data.length}B, ${f.via ?? "online"}) from ${f.pubkey.slice(0, 8)} → ${resolve(dir, safe)}`);
|
|
929
|
+
this.messageStore?.appendFile(f.pubkey, "in", { name: safe, size: f.data.length });
|
|
930
|
+
this.friendMeta?.ensure(f.pubkey);
|
|
931
|
+
this.ipcEvents.emit("event", { type: "chat", userid: f.pubkey, dir: "in" });
|
|
932
|
+
})().catch((e) => this.logger.warn(`Failed to save inline file: ${e.message}`));
|
|
933
|
+
});
|
|
901
934
|
this.peerManager.on("friend-request", (req) => {
|
|
902
935
|
void pubkeyHexToUserid(req.pubkey).then((userid) => {
|
|
903
936
|
const who = `${req.name || "(unnamed)"} ${userid}`;
|
|
@@ -1139,8 +1172,8 @@ export class DaemonServer {
|
|
|
1139
1172
|
/** Append a chat message to the on-disk message store (persists across
|
|
1140
1173
|
* restarts). Also ensures a friend-meta entry exists so the friend shows up
|
|
1141
1174
|
* in the UI list even before it's been renamed. */
|
|
1142
|
-
logChat(userid, dir, text) {
|
|
1143
|
-
this.messageStore?.append(userid, dir, text);
|
|
1175
|
+
logChat(userid, dir, text, via) {
|
|
1176
|
+
this.messageStore?.append(userid, dir, text, Date.now(), undefined, via);
|
|
1144
1177
|
this.friendMeta?.ensure(userid);
|
|
1145
1178
|
this.ipcEvents.emit("event", { type: "chat", userid, dir });
|
|
1146
1179
|
}
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -307,7 +307,10 @@ function useDaemonData() {
|
|
|
307
307
|
pct: m.file.status === "sending" && m.file.size ? Math.min(100, (m.file.sent || 0) / m.file.size * 100) : void 0,
|
|
308
308
|
kbps: m.file.kbps
|
|
309
309
|
} : void 0,
|
|
310
|
-
status: m.dir === "out" ? m.status === "queued" ? "queued" : "read" : void 0
|
|
310
|
+
status: m.dir === "out" ? m.status === "queued" ? "queued" : "read" : void 0,
|
|
311
|
+
// Delivery path: "online" = live session, "offline" = express relay.
|
|
312
|
+
// Incoming messages carry this; the bubble colors them differently.
|
|
313
|
+
via: m.via
|
|
311
314
|
}));
|
|
312
315
|
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
313
316
|
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
@@ -1206,15 +1209,25 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, selMode, selected, onT
|
|
|
1206
1209
|
borderRadius: 12,
|
|
1207
1210
|
borderBottomRightRadius: mine ? 4 : 12,
|
|
1208
1211
|
borderBottomLeftRadius: mine ? 12 : 4,
|
|
1209
|
-
|
|
1212
|
+
// Incoming messages are tinted by delivery path so the split is
|
|
1213
|
+
// visible: amber = arrived via the express relay (offline), plain =
|
|
1214
|
+
// live online session. Outgoing keep the accent bubble.
|
|
1215
|
+
background: mine ? "var(--bub-me)" : m.via === "offline" ? "rgba(245,158,11,0.16)" : "var(--bub-them)",
|
|
1210
1216
|
color: mine ? "#fff" : "var(--text)",
|
|
1211
|
-
border: "1px solid " + (mine ? "transparent" : "var(--line)"),
|
|
1217
|
+
border: "1px solid " + (mine ? "transparent" : m.via === "offline" ? "rgba(245,158,11,0.55)" : "var(--line)"),
|
|
1212
1218
|
fontFamily: "var(--ui)",
|
|
1213
1219
|
fontSize: 13.5,
|
|
1214
1220
|
lineHeight: 1.4,
|
|
1215
1221
|
letterSpacing: -0.1,
|
|
1216
1222
|
wordBreak: "break-word"
|
|
1217
|
-
} }, 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), !
|
|
1223
|
+
} }, 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.via && /* @__PURE__ */ React.createElement(
|
|
1224
|
+
"span",
|
|
1225
|
+
{
|
|
1226
|
+
title: m.via === "offline" ? "delivered via express relay (offline)" : "delivered over a live session (online)",
|
|
1227
|
+
style: { fontFamily: "var(--mono)", fontSize: 9, color: m.via === "offline" ? "#f59e0b" : "var(--faint)" }
|
|
1228
|
+
},
|
|
1229
|
+
m.via === "offline" ? "\u79BB\u7EBF" : "\u5728\u7EBF"
|
|
1230
|
+
), !selMode && m.id && /* @__PURE__ */ React.createElement(
|
|
1218
1231
|
"button",
|
|
1219
1232
|
{
|
|
1220
1233
|
title: T.delete || "delete",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.157",
|
|
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.13",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.82",
|
|
83
83
|
"ink": "^5.2.1",
|
|
84
84
|
"js-yaml": "^4.1.0",
|
|
85
85
|
"node-forge": "^1.4.0",
|