@decentnetwork/lan 0.1.97 → 0.1.99

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.
@@ -51,6 +51,11 @@ 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>;
54
59
  /** Mark a conversation read up to `ts` (defaults to now) — clears unread. */
55
60
  chatMarkRead: (userid: string, ts?: number) => Promise<void>;
56
61
  /** Re-read proxy allowlist from config and apply it to the running
@@ -69,12 +74,14 @@ export interface IpcHandlers {
69
74
  selfRestart: () => Promise<Record<string, unknown>>;
70
75
  }
71
76
  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";
77
+ op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "set-profile" | "chat-mark-read" | "proxy-reload" | "self-restart";
73
78
  address?: string;
74
79
  hello?: string;
75
80
  userid?: string;
76
81
  text?: string;
77
82
  alias?: string;
83
+ name?: string;
84
+ description?: string;
78
85
  before?: number;
79
86
  limit?: number;
80
87
  ts?: number;
@@ -166,6 +166,10 @@ 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
+ }
169
173
  case "chat-mark-read": {
170
174
  if (!req.userid)
171
175
  throw new Error("userid is required");
@@ -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,18 @@ 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
+ },
285
298
  chatMarkRead: async (userid, ts) => {
286
299
  this.friendMeta?.markRead(userid, ts);
287
300
  },
@@ -365,7 +378,7 @@ export class DaemonServer {
365
378
  }));
366
379
  return {
367
380
  identity: this.peerManager?.getIdentity(),
368
- node: { name: this.config.node.name },
381
+ node: { name: this.config.node.name, statusMessage: this.config.node.statusMessage ?? "" },
369
382
  tun: this.tunDevice?.getConfig(),
370
383
  allocatedIp: this.doraIntegration?.getAllocatedIp() ?? this.config.network.ip,
371
384
  dht: this.peerManager?.getDhtHealth() ?? null,
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;
@@ -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 ProfileTab({ T, me }) {
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
- 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)), /* @__PURE__ */ React.createElement(Btn, { icon: "edit" }, 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) }));
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
- handle: node.name ? `@decentnetwork/${node.name}` : "@decentnetwork/peer",
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.97",
3
+ "version": "0.1.99",
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.40",
82
+ "@decentnetwork/peer": "^0.1.42",
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",