@decentnetwork/lan 0.1.87 → 0.1.89

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.
Binary file
Binary file
Binary file
Binary file
@@ -13,6 +13,9 @@ export interface PeerManagerOptions {
13
13
  expressNodes?: BootstrapNode[];
14
14
  /** Use express only for friend-request bootstrap, never for data-plane sendText. */
15
15
  expressControlPlaneOnly?: boolean;
16
+ /** Display name advertised to friends (this node's name, so friend lists show
17
+ * "cn"/"tokyo"/"mac-dev" instead of the generic "@decentnetwork/peer"). */
18
+ nickname?: string;
16
19
  }
17
20
  export declare class PeerManager extends EventEmitter {
18
21
  private peer;
@@ -26,6 +26,7 @@ export class PeerManager extends EventEmitter {
26
26
  bootstrapNodes: opts.bootstrapNodes,
27
27
  expressNodes: opts.expressNodes,
28
28
  expressControlPlaneOnly: opts.expressControlPlaneOnly,
29
+ nickname: opts.nickname,
29
30
  // Register our IP channel (163) as the SDK's bulk-data stream so it
30
31
  // rides a single transport instead of fanning out over UDP + relay +
31
32
  // TCP relay (which delivered 3-4 duplicates of every IP packet).
package/dist/cli/index.js CHANGED
File without changes
@@ -181,6 +181,9 @@ export class DaemonServer {
181
181
  bootstrapNodes: this.config.carrier.bootstrapNodes,
182
182
  expressNodes: this.config.carrier.expressNodes ?? [],
183
183
  expressControlPlaneOnly: true,
184
+ // Advertise this node's name so friends see "cn"/"tokyo"/"mac-dev"
185
+ // instead of the generic "@decentnetwork/peer".
186
+ nickname: this.config.node.name,
184
187
  });
185
188
  await this.peerManager.start();
186
189
  this.logger.info(`Identity: ${this.peerManager.getAddress()}`);
@@ -235,10 +238,13 @@ export class DaemonServer {
235
238
  const uid = f.carrierId ?? f.pubkey ?? "";
236
239
  const meta = this.friendMeta?.get(uid);
237
240
  const lastMsg = last.get(uid);
241
+ // The build-default nickname is useless (every node sends it) —
242
+ // treat it as no-name so the UI falls back to alias/userid.
243
+ const realName = f.name && f.name !== "@decentnetwork/peer" ? f.name : undefined;
238
244
  return {
239
245
  userid: uid,
240
246
  alias: meta?.alias,
241
- name: meta?.alias || f.name || uid,
247
+ name: meta?.alias || realName || uid,
242
248
  status: f.status,
243
249
  lastSeen: f.lastSeen,
244
250
  pinned: meta?.pinned ?? false,
package/dist/ui/server.js CHANGED
@@ -62,8 +62,22 @@ export function startFriendUi(opts) {
62
62
  sendJson(res, 200, { me, friends, pending: pend });
63
63
  return;
64
64
  }
65
+ // Rich friend list (alias / status / unread / last message). Falls back
66
+ // to the diag-derived list on older daemons that lack the op.
67
+ if (req.method === "GET" && url === "/api/friends-list") {
68
+ const r = await opts.call({ op: "friends-list" });
69
+ if (r.ok) {
70
+ sendJson(res, 200, r.data ?? { friends: [] });
71
+ return;
72
+ }
73
+ const diag = await opts.call({ op: "diag" });
74
+ const friends = diag.ok ? (diag.data?.friends ?? []) : [];
75
+ sendJson(res, 200, { friends });
76
+ return;
77
+ }
65
78
  if (req.method === "GET" && url === "/api/chat-history") {
66
- const r = await opts.call({ op: "chat-history" });
79
+ const peer = new URL(req.url || "/", "http://x").searchParams.get("peer") || undefined;
80
+ const r = await opts.call({ op: "chat-history", userid: peer });
67
81
  sendJson(res, 200, r.ok ? (r.data ?? { chats: {} }) : { chats: {} });
68
82
  return;
69
83
  }
@@ -73,6 +87,24 @@ export function startFriendUi(opts) {
73
87
  sendJson(res, r.ok ? 200 : 400, r);
74
88
  return;
75
89
  }
90
+ if (req.method === "POST" && url === "/api/chat-mark-read") {
91
+ const { userid } = await readBody(req);
92
+ const r = await opts.call({ op: "chat-mark-read", userid });
93
+ sendJson(res, r.ok ? 200 : 400, r);
94
+ return;
95
+ }
96
+ if (req.method === "POST" && url === "/api/friend-remove") {
97
+ const { userid } = await readBody(req);
98
+ const r = await opts.call({ op: "friend-remove", userid });
99
+ sendJson(res, r.ok ? 200 : 400, r);
100
+ return;
101
+ }
102
+ if (req.method === "POST" && url === "/api/friend-alias") {
103
+ const { userid, alias } = await readBody(req);
104
+ const r = await opts.call({ op: "friend-set-alias", userid, alias });
105
+ sendJson(res, r.ok ? 200 : 400, r);
106
+ return;
107
+ }
76
108
  if (req.method === "GET" && url === "/api/routes") {
77
109
  let routes = { regions: [], default: "direct" };
78
110
  if (existsSync(opts.routesPath)) {
@@ -205,13 +237,19 @@ function toast(msg){ const t=document.getElementById('toast'); t.textContent=msg
205
237
 
206
238
  async function api(path, body){ const r = await fetch(path, body?{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)}:{}); return r.json(); }
207
239
 
208
- window.friendsById = {};
240
+ window.friendsById = {}; window.me = {};
209
241
  async function refresh(){
210
- let s; try { s = await api('/api/state'); } catch(e){ return; }
211
- const pend = s.pending || [], fr = s.friends || [], me = s.me || {};
212
- document.getElementById('me').textContent = me.ip
213
- ? ('My IP: ' + me.ip + (me.userid ? ' · ' + short(me.userid) : ''))
214
- : (me.userid ? ('userid: ' + short(me.userid) + ' · lan/TUN not up') : 'daemon: no identity');
242
+ let s, fl;
243
+ try { [s, fl] = await Promise.all([api('/api/state'), api('/api/friends-list')]); } catch(e){ return; }
244
+ const pend = s.pending || [], me = s.me || {};
245
+ const fr = (fl && fl.friends) || s.friends || [];
246
+ window.me = me;
247
+ const idStr = me.address || me.userid || '';
248
+ document.getElementById('me').innerHTML =
249
+ (me.ip ? ('My IP: <b>' + esc(me.ip) + '</b> · ') : (me.userid ? '' : 'daemon: no identity '))
250
+ + (me.userid ? ('<span title="'+esc(me.userid)+'">'+esc(short(me.userid))+'</span> ') : '')
251
+ + (idStr ? '<button onclick="copyId()" style="padding:.1rem .5rem;font-size:.75rem">copy my address</button>' : '')
252
+ + (me.userid && !me.installed ? ' · <span style="color:#e67e22">lan/TUN not up</span>' : '');
215
253
  fr.forEach(f => { window.friendsById[f.userid||f.carrierId] = f; });
216
254
  document.getElementById('pcount').textContent = pend.length ? '('+pend.length+')' : '';
217
255
  document.getElementById('fcount').textContent = fr.length ? '('+fr.length+')' : '';
@@ -222,13 +260,39 @@ async function refresh(){
222
260
  <button class="accept" onclick="act('accept','\${esc(p.userid)}')">Accept</button>
223
261
  <button class="reject" onclick="act('reject','\${esc(p.userid)}')">Reject</button>
224
262
  </div>\`).join('') : '<div class="empty">No pending requests.</div>';
225
- document.getElementById('friends').innerHTML = fr.length ? fr.map(f => \`
226
- <div class="row" style="cursor:pointer" onclick="openChat('\${esc(f.userid||f.carrierId)}')" title="open chat">
227
- <span class="dot \${esc(f.status||'offline')}"></span>
228
- <div class="meta"><div class="name">\${esc(f.name || 'unnamed')}</div>
229
- <div class="sub">\${esc(f.status||'')} · \${esc(short(f.userid||f.carrierId))}\${f.virtualIp? ' · '+esc(f.virtualIp):''}</div></div>
230
- </div>\`).join('') : '<div class="empty">No friends yet.</div>';
263
+ document.getElementById('friends').innerHTML = fr.length ? fr.map(f => {
264
+ const uid = f.userid||f.carrierId;
265
+ const nm = friendName(f, uid);
266
+ const lm = f.lastMessage;
267
+ const preview = lm ? ((lm.dir==='out'?'You: ':'') + lm.text) : (f.status||'offline');
268
+ const badge = f.unread ? '<span style="background:#e74c3c;color:#fff;border-radius:10px;padding:0 .45rem;font-size:.7rem;margin-left:.4rem">'+f.unread+'</span>' : '';
269
+ return \`<div class="row" style="cursor:pointer" onclick="openChat('\${esc(uid)}')" title="open chat">
270
+ \${avatar(uid, nm, f.status)}
271
+ <div class="meta"><div class="name">\${esc(nm)}\${badge}</div>
272
+ <div class="sub"><span class="dot \${esc(f.status||'offline')}" style="display:inline-block;vertical-align:middle"></span> \${esc(preview)}</div></div>
273
+ <button onclick="event.stopPropagation();editAlias('\${esc(uid)}')" title="rename" style="padding:.2rem .55rem">✎</button>
274
+ <button class="reject" onclick="event.stopPropagation();delFriend('\${esc(uid)}')" title="remove friend" style="padding:.2rem .55rem">×</button>
275
+ </div>\`;
276
+ }).join('') : '<div class="empty">No friends yet.</div>';
277
+ }
278
+ // The build default "@decentnetwork/peer" is useless (every node sends it) —
279
+ // treat it as no-name and fall back to alias / short userid.
280
+ function friendName(f, uid){
281
+ const n = f.alias || (f.name && f.name !== '@decentnetwork/peer' ? f.name : '');
282
+ return n || short(uid);
283
+ }
284
+ // Deterministic colored-initial avatar from the userid (no protocol/avatar
285
+ // exchange needed yet — same userid always gets the same color + letter).
286
+ function avatar(uid, nm, status){
287
+ const colors=['#e74c3c','#e67e22','#f39c12','#16a085','#27ae60','#2980b9','#8e44ad','#2c3e50','#d35400','#c0392b'];
288
+ let h=0; for(let i=0;i<String(uid).length;i++) h=(h*31+uid.charCodeAt(i))>>>0;
289
+ const c=colors[h%colors.length];
290
+ const ch=((nm||'?').trim().charAt(0)||'?').toUpperCase();
291
+ return '<span style="display:inline-flex;width:2rem;height:2rem;border-radius:50%;background:'+c+';color:#fff;align-items:center;justify-content:center;font-weight:700;flex:0 0 auto">'+esc(ch)+'</span>';
231
292
  }
293
+ function copyId(){ const id=(window.me&&(window.me.address||window.me.userid))||''; if(id&&navigator.clipboard){ navigator.clipboard.writeText(id); toast('Your address copied — share it so others can add you'); } }
294
+ async function delFriend(uid){ if(!confirm('Remove this friend and your messages with them?')) return; const r=await api('/api/friend-remove',{userid:uid}); toast(r.ok?'Removed':(r.error||'failed')); if(chatWith===uid) closeChat(); refresh(); }
295
+ async function editAlias(uid){ const cur=(window.friendsById[uid]||{}).alias||''; const a=prompt('Local name for this friend (empty to clear):', cur); if(a===null) return; const r=await api('/api/friend-alias',{userid:uid,alias:a}); toast(r.ok?'Saved':(r.error||'failed')); refresh(); }
232
296
  async function act(kind, userid){ const r = await api('/api/'+kind, {userid}); toast(r.ok? (kind==='accept'?'Accepted':'Rejected') : (r.error||'failed')); refresh(); }
233
297
  async function addFriend(){ const a=document.getElementById('addr'); const v=a.value.trim(); if(!v) return; const r=await api('/api/add',{address:v}); toast(r.ok?'Friend-request sent':(r.error||'failed')); if(r.ok) a.value=''; refresh(); }
234
298
  document.getElementById('addr').addEventListener('keydown', e=>{ if(e.key==='Enter') addFriend(); });
@@ -237,9 +301,10 @@ let chatWith = null, chatTimer = null;
237
301
  async function openChat(userid){
238
302
  chatWith = userid;
239
303
  const f = window.friendsById[userid] || {};
240
- document.getElementById('chatName').textContent = f.name || 'unnamed';
304
+ document.getElementById('chatName').textContent = friendName(f, userid);
241
305
  document.getElementById('chatSub').textContent = (f.status||'') + ' · ' + short(userid);
242
306
  document.getElementById('chat').style.display = 'block';
307
+ api('/api/chat-mark-read', {userid}); // clears the unread badge
243
308
  await renderChat();
244
309
  clearInterval(chatTimer); chatTimer = setInterval(renderChat, 2000);
245
310
  document.getElementById('chatInput').focus();
@@ -247,7 +312,7 @@ async function openChat(userid){
247
312
  function closeChat(){ chatWith = null; clearInterval(chatTimer); document.getElementById('chat').style.display='none'; refresh(); }
248
313
  async function renderChat(){
249
314
  if(!chatWith) return;
250
- let h; try { h = await api('/api/chat-history'); } catch(e){ return; }
315
+ let h; try { h = await api('/api/chat-history?peer='+encodeURIComponent(chatWith)); } catch(e){ return; }
251
316
  const msgs = (h.chats && h.chats[chatWith]) || [];
252
317
  const log = document.getElementById('chatLog');
253
318
  log.innerHTML = msgs.length ? msgs.map(m => \`<div style="align-self:\${m.dir==='out'?'flex-end':'flex-start'};max-width:75%;padding:.4rem .7rem;border-radius:12px;background:\${m.dir==='out'?'#3478f6':'#8883'};color:\${m.dir==='out'?'#fff':'inherit'}">\${esc(m.text)}</div>\`).join('') : '<div class="empty">No messages yet — say hi.</div>';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
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",
61
+ "build": "tsc -p tsconfig.json && chmod +x dist/cli/index.js",
62
62
  "build:helper": "cd helper/tun-helper && go build -o ../../bin/tun-helper-$(go env GOOS)-$(go env GOARCH) .",
63
63
  "build:helper:linux-amd64": "cd helper/tun-helper && GOOS=linux GOARCH=amd64 go build -o ../../bin/tun-helper-linux-amd64 .",
64
64
  "build:helper:linux-arm64": "cd helper/tun-helper && GOOS=linux GOARCH=arm64 go build -o ../../bin/tun-helper-linux-arm64 .",
@@ -77,7 +77,7 @@
77
77
  },
78
78
  "dependencies": {
79
79
  "@decentnetwork/dora": "^0.1.6",
80
- "@decentnetwork/peer": "^0.1.39",
80
+ "@decentnetwork/peer": "^0.1.40",
81
81
  "js-yaml": "^4.1.0",
82
82
  "yargs": "^17.7.2"
83
83
  },