@decentnetwork/lan 0.1.98 → 0.1.100
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 +15 -0
- package/dist/carrier/peer-manager.js +29 -0
- package/dist/cli/commands.d.ts +21 -0
- package/dist/cli/commands.js +29 -0
- package/dist/cli/index.js +15 -1
- package/dist/console/console.js +1011 -0
- package/dist/daemon/ipc.d.ts +11 -1
- package/dist/daemon/ipc.js +11 -0
- package/dist/daemon/server.js +46 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/desktop/app.js +32 -6
- package/dist/ui/server.js +9 -1
- package/package.json +7 -5
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -51,6 +51,13 @@ export interface IpcHandlers {
|
|
|
51
51
|
friendRemove: (userid: string) => Promise<void>;
|
|
52
52
|
/** Set/clear a local display alias for a friend. */
|
|
53
53
|
friendSetAlias: (userid: string, alias?: string) => Promise<void>;
|
|
54
|
+
/** Update our own profile (display name + description); persists + re-pushes. */
|
|
55
|
+
setProfile: (info: {
|
|
56
|
+
name?: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
}) => Promise<void>;
|
|
59
|
+
/** Offer a local file (by path) to a friend over toxcore file transfer. */
|
|
60
|
+
fileSend: (userid: string, path: string) => Promise<Record<string, unknown>>;
|
|
54
61
|
/** Mark a conversation read up to `ts` (defaults to now) — clears unread. */
|
|
55
62
|
chatMarkRead: (userid: string, ts?: number) => Promise<void>;
|
|
56
63
|
/** Re-read proxy allowlist from config and apply it to the running
|
|
@@ -69,12 +76,15 @@ export interface IpcHandlers {
|
|
|
69
76
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
70
77
|
}
|
|
71
78
|
export interface IpcRequest {
|
|
72
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "chat-mark-read" | "proxy-reload" | "self-restart";
|
|
79
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "set-profile" | "file-send" | "chat-mark-read" | "proxy-reload" | "self-restart";
|
|
73
80
|
address?: string;
|
|
74
81
|
hello?: string;
|
|
75
82
|
userid?: string;
|
|
76
83
|
text?: string;
|
|
77
84
|
alias?: string;
|
|
85
|
+
name?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
path?: string;
|
|
78
88
|
before?: number;
|
|
79
89
|
limit?: number;
|
|
80
90
|
ts?: number;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -166,6 +166,17 @@ export class IpcServer {
|
|
|
166
166
|
await this.handlers.friendSetAlias(req.userid, req.alias);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
+
case "set-profile": {
|
|
170
|
+
await this.handlers.setProfile({ name: req.name, description: req.description });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
case "file-send": {
|
|
174
|
+
if (!req.userid)
|
|
175
|
+
throw new Error("userid is required");
|
|
176
|
+
if (!req.path)
|
|
177
|
+
throw new Error("path is required");
|
|
178
|
+
return await this.handlers.fileSend(req.userid, req.path);
|
|
179
|
+
}
|
|
169
180
|
case "chat-mark-read": {
|
|
170
181
|
if (!req.userid)
|
|
171
182
|
throw new Error("userid is required");
|
package/dist/daemon/server.js
CHANGED
|
@@ -194,6 +194,7 @@ export class DaemonServer {
|
|
|
194
194
|
// Advertise this node's name so friends see "cn"/"tokyo"/"mac-dev"
|
|
195
195
|
// instead of the generic "@decentnetwork/peer".
|
|
196
196
|
nickname: this.config.node.name,
|
|
197
|
+
statusMessage: this.config.node.statusMessage,
|
|
197
198
|
});
|
|
198
199
|
await this.peerManager.start();
|
|
199
200
|
this.logger.info(`Identity: ${this.peerManager.getAddress()}`);
|
|
@@ -282,6 +283,29 @@ export class DaemonServer {
|
|
|
282
283
|
friendSetAlias: async (userid, alias) => {
|
|
283
284
|
this.friendMeta?.setAlias(userid, alias);
|
|
284
285
|
},
|
|
286
|
+
setProfile: async ({ name, description }) => {
|
|
287
|
+
// Re-push to friends over Carrier (live) …
|
|
288
|
+
this.peerManager?.setUserInfo({ name, description });
|
|
289
|
+
// … and persist so it survives a daemon restart.
|
|
290
|
+
if (typeof name === "string" && name.trim())
|
|
291
|
+
this.config.node.name = name.trim();
|
|
292
|
+
if (typeof description === "string")
|
|
293
|
+
this.config.node.statusMessage = description;
|
|
294
|
+
await ConfigLoader.save(this.config, resolve(this.configDir, "config.yaml")).catch((e) => {
|
|
295
|
+
this.logger.warn(`Failed to persist profile: ${e.message}`);
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
fileSend: async (userid, path) => {
|
|
299
|
+
const fs = await import("fs/promises");
|
|
300
|
+
const { basename } = await import("path");
|
|
301
|
+
const data = await fs.readFile(path);
|
|
302
|
+
const name = basename(path);
|
|
303
|
+
const fileId = this.peerManager?.sendFile(userid, new Uint8Array(data), name);
|
|
304
|
+
if (!fileId)
|
|
305
|
+
throw new Error("No free transfer slot (or friend unknown)");
|
|
306
|
+
this.logger.info(`Offering file "${name}" (${data.length}B) to ${userid.slice(0, 8)}`);
|
|
307
|
+
return { fileId, name, size: data.length };
|
|
308
|
+
},
|
|
285
309
|
chatMarkRead: async (userid, ts) => {
|
|
286
310
|
this.friendMeta?.markRead(userid, ts);
|
|
287
311
|
},
|
|
@@ -365,7 +389,7 @@ export class DaemonServer {
|
|
|
365
389
|
}));
|
|
366
390
|
return {
|
|
367
391
|
identity: this.peerManager?.getIdentity(),
|
|
368
|
-
node: { name: this.config.node.name },
|
|
392
|
+
node: { name: this.config.node.name, statusMessage: this.config.node.statusMessage ?? "" },
|
|
369
393
|
tun: this.tunDevice?.getConfig(),
|
|
370
394
|
allocatedIp: this.doraIntegration?.getAllocatedIp() ?? this.config.network.ip,
|
|
371
395
|
dht: this.peerManager?.getDhtHealth() ?? null,
|
|
@@ -531,6 +555,27 @@ export class DaemonServer {
|
|
|
531
555
|
this.peerManager.on("message", (pubkey, text) => {
|
|
532
556
|
this.logChat(pubkey, "in", text);
|
|
533
557
|
});
|
|
558
|
+
// File transfer (toxcore-standard): auto-accept offers from friends and
|
|
559
|
+
// save completed files under <configDir>/downloads/.
|
|
560
|
+
this.peerManager.on("file-offer", (o) => {
|
|
561
|
+
this.logger.info(`Incoming file "${o.name}" (${o.size}B) from ${o.friendId.slice(0, 8)} — accepting`);
|
|
562
|
+
this.peerManager?.acceptFile(o.friendId, o.fileNumber);
|
|
563
|
+
});
|
|
564
|
+
this.peerManager.on("file-complete", (p) => {
|
|
565
|
+
if (p.sending || !p.data) {
|
|
566
|
+
if (p.sending)
|
|
567
|
+
this.logger.info(`File "${p.name}" sent to ${p.friendId.slice(0, 8)}`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
void (async () => {
|
|
571
|
+
const dir = resolve(this.configDir, "downloads");
|
|
572
|
+
const fs = await import("fs/promises");
|
|
573
|
+
await fs.mkdir(dir, { recursive: true });
|
|
574
|
+
const safe = (p.name || "file").replace(/[/\\]/g, "_");
|
|
575
|
+
await fs.writeFile(resolve(dir, safe), Buffer.from(p.data));
|
|
576
|
+
this.logger.info(`Saved file "${p.name}" (${p.size}B) from ${p.friendId.slice(0, 8)} → ${resolve(dir, safe)}`);
|
|
577
|
+
})().catch((e) => this.logger.warn(`Failed to save file: ${e.message}`));
|
|
578
|
+
});
|
|
534
579
|
this.peerManager.on("friend-request", (req) => {
|
|
535
580
|
void pubkeyHexToUserid(req.pubkey).then((userid) => {
|
|
536
581
|
const who = `${req.name || "(unnamed)"} ${userid}`;
|
package/dist/types.d.ts
CHANGED
|
@@ -102,6 +102,8 @@ export interface FrameEncodingOptions {
|
|
|
102
102
|
export interface NodeConfig {
|
|
103
103
|
name: string;
|
|
104
104
|
namespace: string;
|
|
105
|
+
/** Optional status-message / short bio shown to friends (Carrier USERINFO descr). */
|
|
106
|
+
statusMessage?: string;
|
|
105
107
|
}
|
|
106
108
|
export interface BootstrapNode {
|
|
107
109
|
host: string;
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -185,7 +185,8 @@ const dkApi = {
|
|
|
185
185
|
reject: (userid) => dkPost("/api/reject", { userid }),
|
|
186
186
|
remove: (userid) => dkPost("/api/friend-remove", { userid }),
|
|
187
187
|
alias: (userid, alias) => dkPost("/api/friend-alias", { userid, alias }),
|
|
188
|
-
markRead: (userid) => dkPost("/api/chat-mark-read", { userid })
|
|
188
|
+
markRead: (userid) => dkPost("/api/chat-mark-read", { userid }),
|
|
189
|
+
setProfile: (name, description) => dkPost("/api/set-profile", { name, description })
|
|
189
190
|
};
|
|
190
191
|
const DK_ME_FALLBACK = {
|
|
191
192
|
name: "\u2026",
|
|
@@ -1013,7 +1014,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onAlias, onRe
|
|
|
1013
1014
|
setDraft("");
|
|
1014
1015
|
}
|
|
1015
1016
|
};
|
|
1016
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--bg)" } }, /* @__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 } }), /* @__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: () => {
|
|
1017
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column", background: "var(--bg)" } }, /* @__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 } }), /* @__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: () => {
|
|
1017
1018
|
setMenu(false);
|
|
1018
1019
|
onAlias(peer);
|
|
1019
1020
|
} }), /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
|
|
@@ -1058,7 +1059,7 @@ function ChatEmpty({ T }) {
|
|
|
1058
1059
|
}
|
|
1059
1060
|
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onAlias, onRemove, onOpenNet }) {
|
|
1060
1061
|
const peer = peers.find((p) => p.id === activeId);
|
|
1061
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onAlias, onRemove, onOpenNet }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1062
|
+
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, onAlias, onRemove, onOpenNet }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1062
1063
|
}
|
|
1063
1064
|
Object.assign(window, { ChatTab });
|
|
1064
1065
|
function StatTile({ label, value, sub, tone }) {
|
|
@@ -1117,9 +1118,28 @@ function DkQrModal({ value, label, onClose }) {
|
|
|
1117
1118
|
function Card({ label, children, trailing }) {
|
|
1118
1119
|
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 10 } }, /* @__PURE__ */ React.createElement(Section, { label, trailing }), /* @__PURE__ */ React.createElement("div", { style: { borderRadius: 11, border: "1px solid var(--line)", overflow: "hidden", background: "var(--panel)" } }, children));
|
|
1119
1120
|
}
|
|
1120
|
-
function
|
|
1121
|
+
function DkEditModal({ T, me, onClose, onSave }) {
|
|
1122
|
+
const [name, setName] = React.useState(me.name || "");
|
|
1123
|
+
const [desc, setDesc] = React.useState(me.description || "");
|
|
1124
|
+
const save = () => {
|
|
1125
|
+
const n = name.trim();
|
|
1126
|
+
if (n) onSave(n, desc);
|
|
1127
|
+
};
|
|
1128
|
+
const field = { height: 38, borderRadius: 9, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13.5, padding: "0 12px", outline: "none" };
|
|
1129
|
+
const lbl = { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 };
|
|
1130
|
+
const onKey = (e) => {
|
|
1131
|
+
if (e.key === "Enter") save();
|
|
1132
|
+
if (e.key === "Escape") onClose();
|
|
1133
|
+
};
|
|
1134
|
+
return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 440, maxWidth: "92vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 22, display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, T.editProfile), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "display name"), /* @__PURE__ */ React.createElement("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: onKey, autoFocus: true, maxLength: 48, style: field })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "status message"), /* @__PURE__ */ React.createElement("input", { value: desc, onChange: (e) => setDesc(e.target.value), onKeyDown: onKey, maxLength: 120, placeholder: "optional \u2014 a short bio friends will see", style: field })), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, "Your userid (the unique identity) can't change \u2014 only the display name + status."), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: onClose }, T.cancel || "cancel"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", onClick: save }, T.save || "save"))));
|
|
1135
|
+
}
|
|
1136
|
+
function ProfileTab({ T, me, onEdit }) {
|
|
1121
1137
|
const [qr, setQr] = React.useState(null);
|
|
1122
|
-
|
|
1138
|
+
const [editing, setEditing] = React.useState(false);
|
|
1139
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 18, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.version, value: `lan ${me.lanVer} \xB7 peer ${me.peerVer} \xB7 ${me.channel}`, last: true })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), qr && /* @__PURE__ */ React.createElement(DkQrModal, { value: qr.value, label: qr.label, onClose: () => setQr(null) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
|
|
1140
|
+
if (onEdit) onEdit(name, description);
|
|
1141
|
+
setEditing(false);
|
|
1142
|
+
} }));
|
|
1123
1143
|
}
|
|
1124
1144
|
Object.assign(window, { ProfileTab });
|
|
1125
1145
|
const DK_DEFAULTS = (
|
|
@@ -1360,6 +1380,11 @@ function DkApp() {
|
|
|
1360
1380
|
if (!activeId) return;
|
|
1361
1381
|
data.loadThread(activeId);
|
|
1362
1382
|
dkApi.markRead(activeId).then(data.refresh);
|
|
1383
|
+
const iv = setInterval(() => {
|
|
1384
|
+
data.loadThread(activeId);
|
|
1385
|
+
dkApi.markRead(activeId);
|
|
1386
|
+
}, 2500);
|
|
1387
|
+
return () => clearInterval(iv);
|
|
1363
1388
|
}, [activeId]);
|
|
1364
1389
|
const onSelect = (id) => setActiveId(id);
|
|
1365
1390
|
const onAct = (id, kind) => {
|
|
@@ -1382,6 +1407,7 @@ function DkApp() {
|
|
|
1382
1407
|
const a = window.prompt("Set alias for this peer (empty to clear):", peer.alias || "");
|
|
1383
1408
|
if (a !== null) dkApi.alias(peer.id, a).then(data.refresh);
|
|
1384
1409
|
};
|
|
1410
|
+
const onEdit = (name, description) => dkApi.setProfile(name, description).then(data.refresh);
|
|
1385
1411
|
const onSetExit = () => {
|
|
1386
1412
|
};
|
|
1387
1413
|
const onOpenChat = (id) => {
|
|
@@ -1394,7 +1420,7 @@ function DkApp() {
|
|
|
1394
1420
|
{ id: "network", icon: "network", label: T.network },
|
|
1395
1421
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
1396
1422
|
];
|
|
1397
|
-
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, 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, 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 })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
1423
|
+
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, 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(
|
|
1398
1424
|
TweakRadio,
|
|
1399
1425
|
{
|
|
1400
1426
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
package/dist/ui/server.js
CHANGED
|
@@ -155,7 +155,9 @@ export function startFriendUi(opts) {
|
|
|
155
155
|
const meExit = DEFAULT_EXITS.find((e) => e.userid && e.userid === identity.userid);
|
|
156
156
|
const me = {
|
|
157
157
|
name: node.name || (identity.userid ?? "").slice(0, 8),
|
|
158
|
-
|
|
158
|
+
// Handle reads as a network address: <name>@decentnetwork.
|
|
159
|
+
handle: `${node.name || "peer"}@decentnetwork`,
|
|
160
|
+
description: node.statusMessage ?? "",
|
|
159
161
|
userId: identity.userid ?? "",
|
|
160
162
|
carrier: identity.address ?? "",
|
|
161
163
|
netKey: identity.userid ?? "",
|
|
@@ -293,6 +295,12 @@ export function startFriendUi(opts) {
|
|
|
293
295
|
sendJson(res, r.ok ? 200 : 400, r);
|
|
294
296
|
return;
|
|
295
297
|
}
|
|
298
|
+
if (req.method === "POST" && url === "/api/set-profile") {
|
|
299
|
+
const { name, description } = await readBody(req);
|
|
300
|
+
const r = await opts.call({ op: "set-profile", name, description });
|
|
301
|
+
sendJson(res, r.ok ? 200 : 400, r);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
296
304
|
if (req.method === "GET" && url === "/api/routes") {
|
|
297
305
|
let routes = { regions: [], default: "direct" };
|
|
298
306
|
if (existsSync(opts.routesPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.100",
|
|
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",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
|
-
"build": "tsc -p tsconfig.json && chmod +x dist/cli/index.js && node scripts/build-ui.mjs",
|
|
61
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli/index.js && node scripts/build-ui.mjs && node scripts/build-console.mjs",
|
|
62
62
|
"build:ui": "node scripts/build-ui.mjs",
|
|
63
63
|
"build:helper": "cd helper/tun-helper && go build -o ../../bin/tun-helper-$(go env GOOS)-$(go env GOARCH) .",
|
|
64
64
|
"build:helper:linux-amd64": "cd helper/tun-helper && GOOS=linux GOARCH=amd64 go build -o ../../bin/tun-helper-linux-amd64 .",
|
|
@@ -74,12 +74,15 @@
|
|
|
74
74
|
"test:coverage": "vitest --coverage",
|
|
75
75
|
"typecheck": "tsc --noEmit",
|
|
76
76
|
"lint": "eslint src --ext .ts",
|
|
77
|
-
"prepublishOnly": "rm -rf dist && npm run build && npm run typecheck && npm run build:helpers:all"
|
|
77
|
+
"prepublishOnly": "rm -rf dist && npm run build && npm run typecheck && npm run build:helpers:all",
|
|
78
|
+
"build:console": "node scripts/build-console.mjs"
|
|
78
79
|
},
|
|
79
80
|
"dependencies": {
|
|
80
81
|
"@decentnetwork/dora": "^0.1.6",
|
|
81
|
-
"@decentnetwork/peer": "^0.1.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.43",
|
|
83
|
+
"ink": "^5.2.1",
|
|
82
84
|
"js-yaml": "^4.1.0",
|
|
85
|
+
"react": "^18.3.1",
|
|
83
86
|
"yargs": "^17.7.2"
|
|
84
87
|
},
|
|
85
88
|
"devDependencies": {
|
|
@@ -90,7 +93,6 @@
|
|
|
90
93
|
"@typescript-eslint/parser": "^7.10.0",
|
|
91
94
|
"@vitest/coverage-v8": "^1.6.0",
|
|
92
95
|
"eslint": "^8.57.0",
|
|
93
|
-
"react": "^18.3.1",
|
|
94
96
|
"react-dom": "^18.3.1",
|
|
95
97
|
"ts-node": "^10.9.2",
|
|
96
98
|
"typescript": "^5.4.5",
|