@decentnetwork/beagle 0.1.58 → 0.1.60

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.
@@ -201,6 +201,10 @@ export class EmbeddedHost {
201
201
  // evidence than any status flag, so use it.
202
202
  void this.#chainFlush(pubkey);
203
203
  });
204
+ // `description` is the self-introduction the sender was asked to write —
205
+ // the entire reason for asking is so the recipient knows who is knocking.
206
+ // It arrives on the packet and used to be dropped here, so the request
207
+ // card had nothing to show but a truncated key.
204
208
  this.#node.on("friend-request", (req) => {
205
209
  const userid = req.userid ?? req.pubkey;
206
210
  if (this.#autoAccept) {
@@ -214,6 +218,7 @@ export class EmbeddedHost {
214
218
  userid,
215
219
  pubkey: req.pubkey,
216
220
  name: req.name,
221
+ descr: req.description,
217
222
  hello: req.hello,
218
223
  arrivedAt: Date.now(),
219
224
  });
package/dist/server.d.ts CHANGED
@@ -49,6 +49,10 @@ export interface BeagleServerOptions {
49
49
  pk: string;
50
50
  }>;
51
51
  };
52
+ /** Bridge whose roster the "Here" tab shows. The desktop does not route
53
+ * traffic through it — it only completes the handshake to prove its key
54
+ * and read who else is there. */
55
+ bridgeWs?: string;
52
56
  }
53
57
  export declare function startBeagleServer(opts: BeagleServerOptions): {
54
58
  stop: () => void;
package/dist/server.js CHANGED
@@ -21,6 +21,7 @@ import { INSTALL_COMMAND as LAN_INSTALL_COMMAND } from "./lan-handoff.js";
21
21
  import yaml from "js-yaml";
22
22
  import { DEFAULT_EXITS } from "./exits.js";
23
23
  import * as peerAddr from "@decentnetwork/peer";
24
+ import { bridgeRoster } from "./bridge-roster.js";
24
25
  // Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
25
26
  // scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
26
27
  const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
@@ -54,7 +55,7 @@ async function fetchDiscoverJson(url) {
54
55
  // ── beagles.eth names (ens-gateway worker) ──────────────────────────────────
55
56
  // The same registry the mobile apps register into. Web-side registration
56
57
  // signs the record with THIS node's Carrier identity (XEdDSA via the backend
57
- // `sign` op) — the gateway verifies it the same way Sign in with Decent does.
58
+ // `sign` op) — the gateway verifies it the same way Sign in with Beagle does.
58
59
  const ENS_GATEWAY = process.env.BEAGLE_ENS_GATEWAY || "https://ens-gateway.beaglechat.workers.dev";
59
60
  async function ensFetchJson(path) {
60
61
  const ctl = new AbortController();
@@ -413,7 +414,7 @@ function findSavedFile(downloadsDir, name) {
413
414
  return null;
414
415
  }
415
416
  /** True only when the request came from the local machine. Used to gate the
416
- * "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
417
+ * "Sign in with Beagle" routes so binding the UI to a LAN IP cannot expose
417
418
  * identity signing to other hosts. The popup always runs in the local user's
418
419
  * browser, so it connects over the loopback interface. */
419
420
  function isLocalRequest(req) {
@@ -507,7 +508,7 @@ function validateOrigin(raw) {
507
508
  return null;
508
509
  }
509
510
  }
510
- // The "Sign in with Decent" consent popup. Self-contained (no external assets),
511
+ // The "Sign in with Beagle" consent popup. Self-contained (no external assets),
511
512
  // dark theme matching the desktop UI. Reads origin+nonce from its own query,
512
513
  // shows the requesting site and this node's identity, and on Approve calls the
513
514
  // local /api/connect-approve to sign, then postMessages the result to the
@@ -516,7 +517,7 @@ function validateOrigin(raw) {
516
517
  const CONNECT_PAGE = `<!doctype html>
517
518
  <html lang="en"><head><meta charset="utf-8">
518
519
  <meta name="viewport" content="width=device-width,initial-scale=1">
519
- <title>Sign in with Decent</title>
520
+ <title>Sign in with Beagle</title>
520
521
  <style>
521
522
  :root{--bg:#0c0d11;--panel:#14161d;--line:#262a35;--text:#e7e9ef;--faint:#8b91a3;
522
523
  --accent:#5b8cff;--good:#46a758;--bad:#e5604d;--mono:ui-monospace,SFMono-Regular,Menlo,monospace}
@@ -533,7 +534,9 @@ const CONNECT_PAGE = `<!doctype html>
533
534
  .origin{color:var(--text);font-weight:600;word-break:break-all}
534
535
  .id{background:#0e1016;border:1px solid var(--line);border-radius:10px;padding:11px 12px;margin-bottom:14px}
535
536
  .lbl{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--faint);margin-bottom:4px}
537
+ .who{font-size:13.5px;font-weight:600;margin-bottom:3px}
536
538
  .userid{font-family:var(--mono);font-size:12.5px;color:var(--text);word-break:break-all}
539
+ [hidden]{display:none !important}
537
540
  .err{background:rgba(229,96,77,.12);border:1px solid rgba(229,96,77,.4);color:#ffb4a8;
538
541
  border-radius:9px;padding:9px 11px;font-size:12.5px;margin-bottom:13px}
539
542
  .row{display:flex;gap:10px;margin-top:4px}
@@ -546,10 +549,14 @@ const CONNECT_PAGE = `<!doctype html>
546
549
  </style></head>
547
550
  <body>
548
551
  <div class="card">
549
- <div class="brand"><span class="d"></span>Decent</div>
552
+ <div class="brand"><span class="d"></span>Beagle</div>
550
553
  <h1>Sign in</h1>
551
- <p class="sub"><span id="origin" class="origin">…</span> wants to sign you in with your Decent identity.</p>
552
- <div class="id"><div class="lbl">Your identity</div><div id="userid" class="userid">loading…</div></div>
554
+ <p class="sub"><span id="origin" class="origin">…</span> wants to sign you in with your Beagle identity.</p>
555
+ <div class="id">
556
+ <div class="lbl">Your identity</div>
557
+ <div id="who" class="who" hidden></div>
558
+ <div id="userid" class="userid">loading…</div>
559
+ </div>
553
560
  <div id="err" class="err" hidden></div>
554
561
  <div class="row">
555
562
  <button id="deny" class="btn ghost">Deny</button>
@@ -571,10 +578,11 @@ const CONNECT_PAGE = `<!doctype html>
571
578
  if(!validOrigin) fail("This sign-in request has an invalid origin and was blocked.");
572
579
  if(nonce.length===0||nonce.length>512){ validOrigin=false; fail("This sign-in request is missing a valid nonce."); }
573
580
  fetch("/api/state").then(function(r){return r.json();}).then(function(s){
574
- var uid=(s.me&&s.me.userid)||"";
581
+ var uid=(s.me&&s.me.userid)||"", nm=(s.me&&s.me.name)||"";
575
582
  document.getElementById("userid").textContent=uid||"(no identity)";
583
+ if(nm){var w=document.getElementById("who"); w.textContent=nm; w.hidden=false;}
576
584
  if(uid&&validOrigin) approve.disabled=false;
577
- else if(!uid) fail("No local Decent identity found — is the daemon running?");
585
+ else if(!uid) fail("No local Beagle identity found — is the daemon running?");
578
586
  }).catch(function(){ fail("Could not reach the local agentnet daemon."); });
579
587
  deny.onclick=function(){ reply({error:"denied"}); setTimeout(function(){window.close();},60); };
580
588
  approve.onclick=function(){
@@ -583,7 +591,7 @@ const CONNECT_PAGE = `<!doctype html>
583
591
  body:JSON.stringify({origin:origin,nonce:nonce})})
584
592
  .then(function(r){return r.json();})
585
593
  .then(function(j){ if(!j.ok) throw new Error(j.error||"sign failed");
586
- reply({userid:j.userid,sig:j.sig}); setTimeout(function(){window.close();},60); })
594
+ reply({userid:j.userid,sig:j.sig,name:j.name,avatar:j.avatar}); setTimeout(function(){window.close();},60); })
587
595
  .catch(function(e){ approve.disabled=false; approve.textContent="Approve"; fail("Signing failed: "+e.message); });
588
596
  };
589
597
  })();
@@ -678,7 +686,7 @@ export function startBeagleServer(opts) {
678
686
  res.end("not found");
679
687
  return;
680
688
  }
681
- // ── Sign in with Decent ────────────────────────────────────────────
689
+ // ── Sign in with Beagle ────────────────────────────────────────────
682
690
  // A website opens http://localhost:8765/connect?origin=…&nonce=… in a
683
691
  // popup; on Approve we sign "decent-auth\n<origin>\n<nonce>" with this
684
692
  // node's Carrier identity and postMessage {userid,nonce,sig} back to the
@@ -689,7 +697,7 @@ export function startBeagleServer(opts) {
689
697
  if (url === "/connect" || url === "/api/connect-approve") {
690
698
  if (!isLocalRequest(req)) {
691
699
  res.writeHead(403, { "content-type": "text/plain" });
692
- res.end("Sign in with Decent is available only on this machine (localhost).");
700
+ res.end("Sign in with Beagle is available only on this machine (localhost).");
693
701
  return;
694
702
  }
695
703
  }
@@ -718,11 +726,30 @@ export function startBeagleServer(opts) {
718
726
  sendJson(res, 502, { ok: false, error: r.error || "sign failed" });
719
727
  return;
720
728
  }
721
- sendJson(res, 200, {
722
- ok: true,
723
- userid: r.data?.userid ?? "",
724
- sig: r.data?.sig ?? "",
725
- });
729
+ const userid = r.data?.userid ?? "";
730
+ // Name and avatar ride along so the site can render the person without
731
+ // a second lookup it has no way to make — nothing public maps a userid
732
+ // to a display name unless that user registered a beagles.eth name.
733
+ // Additive: a caller that ignores them behaves exactly as before, and
734
+ // neither is part of what the signature proves.
735
+ let name = "";
736
+ let avatar = null;
737
+ try {
738
+ const diag = await opts.call({ op: "diag" });
739
+ name = ((diag.ok ? diag.data : {})?.node?.name) ?? "";
740
+ const pub = (await ensPublicByUserid(1200)).get(userid);
741
+ if (pub) {
742
+ if (!name)
743
+ name = pub.displayName ?? "";
744
+ avatar = pub.avatarUrl ?? null;
745
+ if (!avatar && pub.punkId != null) {
746
+ const punk = await punksFetch(`/api/punks/${pub.punkId}`).catch(() => null);
747
+ avatar = punk?.image ?? null;
748
+ }
749
+ }
750
+ }
751
+ catch { /* identity is what matters; the trimmings are best-effort */ }
752
+ sendJson(res, 200, { ok: true, userid, sig: r.data?.sig ?? "", name, avatar });
726
753
  return;
727
754
  }
728
755
  // ── beagles.eth name registration ──
@@ -760,7 +787,13 @@ export function startBeagleServer(opts) {
760
787
  sendJson(res, 200, { ok: true, punk });
761
788
  }
762
789
  catch (err) {
763
- sendJson(res, 502, { ok: false, error: String(err?.message ?? err) });
790
+ // An id the set does not contain is a normal negative answer, not a
791
+ // gateway failure. Reporting 502 made a bad stored punkId look like
792
+ // the punks service was down, and the client — which only cached
793
+ // successes — retried it on every render.
794
+ const msg = String(err?.message ?? err);
795
+ const missing = /\b404\b|not found/i.test(msg);
796
+ sendJson(res, missing ? 404 : 502, { ok: false, error: missing ? "no such punk" : msg });
764
797
  }
765
798
  return;
766
799
  }
@@ -1152,6 +1185,9 @@ export function startBeagleServer(opts) {
1152
1185
  }
1153
1186
  const q = new URL(req.url || "", "http://x").searchParams;
1154
1187
  const userid = q.get("userid") || "";
1188
+ // The sender's own copy needs saving too, or its chip renders as an
1189
+ // empty 0 B card: the bytes went peer-to-peer and never touched here.
1190
+ const dir = q.get("dir") === "out" ? "out" : "in";
1155
1191
  const rawName = q.get("name") || "agentnet-file";
1156
1192
  const safe = rawName.replace(/[/\\]/g, "_").slice(0, 200) || "agentnet-file";
1157
1193
  if (!userid) {
@@ -1178,7 +1214,7 @@ export function startBeagleServer(opts) {
1178
1214
  ws.on("finish", () => resolve2());
1179
1215
  });
1180
1216
  const st = statSync(finalPath);
1181
- const r = await opts.call({ op: "file-log-local", userid, dir: "in", name: finalName, size: st.size });
1217
+ const r = await opts.call({ op: "file-log-local", userid, dir, name: finalName, size: st.size });
1182
1218
  if (!r.ok) {
1183
1219
  sendJson(res, 502, r);
1184
1220
  return;
@@ -1311,9 +1347,13 @@ export function startBeagleServer(opts) {
1311
1347
  const d = diag.ok ? (diag.data ?? {}) : {};
1312
1348
  const identity = d.identity ?? {};
1313
1349
  const tun = d.tun ?? {};
1350
+ const node = d.node ?? {};
1314
1351
  const me = {
1315
1352
  userid: identity.userid,
1316
1353
  address: identity.address,
1354
+ // The sign-in popup shows this above the key: a userid alone does not
1355
+ // tell you which of your identities is about to answer.
1356
+ name: node.name ?? "",
1317
1357
  // The configured TUN address is what this host can actually route.
1318
1358
  // Prefer it over a stale init/default allocation during Dora outage.
1319
1359
  ip: tun.ip ?? d.allocatedIp,
@@ -1359,6 +1399,32 @@ export function startBeagleServer(opts) {
1359
1399
  }
1360
1400
  return;
1361
1401
  }
1402
+ if (req.method === "GET" && url === "/api/bridge-roster") {
1403
+ // Same shape the browser build returns, so discover.jsx renders it
1404
+ // without knowing which client it is running in.
1405
+ if (!opts.bridgeWs || !opts.callIce?.keyFile) {
1406
+ return sendJson(res, 200, { ok: true, list: [], pending: true });
1407
+ }
1408
+ try {
1409
+ // diag already carries both facts, so there is no second round trip
1410
+ // and no op to add: the address is the identity, the nickname is the
1411
+ // node's own name.
1412
+ const diag = (await opts.call({ op: "diag" }));
1413
+ const r = await bridgeRoster(opts.bridgeWs, opts.callIce.keyFile, {
1414
+ name: diag?.node?.name,
1415
+ address: diag?.identity?.address,
1416
+ });
1417
+ if (!r)
1418
+ return sendJson(res, 200, { ok: true, list: [], pending: true });
1419
+ return sendJson(res, 200, {
1420
+ ok: true, bridge: r.bridge, online: r.online,
1421
+ list: r.peers.map((p) => ({ ...p, punkId: p.punk ?? null })),
1422
+ });
1423
+ }
1424
+ catch (err) {
1425
+ return sendJson(res, 200, { ok: false, error: String(err?.message ?? err) });
1426
+ }
1427
+ }
1362
1428
  if (req.method === "GET" && url === "/api/discover-registered") {
1363
1429
  try {
1364
1430
  const raw = (await fetchDiscoverJson(`${ENS_GATEWAY}/names`));
@@ -1475,8 +1541,28 @@ export function startBeagleServer(opts) {
1475
1541
  if (req.method === "GET" && url === "/api/ens-profile") {
1476
1542
  const q = new URL(req.url || "", "http://localhost").searchParams;
1477
1543
  let userid = q.get("userid") || "";
1544
+ // Also accept ?name= (a beagles.eth display name), so a group sender
1545
+ // known only by nickname still resolves to its avatar — the userid is
1546
+ // simply not available for those.
1547
+ const qname = q.get("name") || "";
1548
+ if (!userid && qname) {
1549
+ const norm = (x) => String(x || "").toLowerCase().replace(/\s+/g, "");
1550
+ for (const [uid, prof] of await ensPublicByUserid(1500)) {
1551
+ if (norm(prof.displayName || "") === norm(qname) || norm(prof.ens || "") === norm(qname)) {
1552
+ userid = uid;
1553
+ break;
1554
+ }
1555
+ }
1556
+ }
1557
+ // A name nobody has registered is a lookup that found nothing, not a
1558
+ // bad request: 400 makes the caller log an error and, worse, retry.
1559
+ // Only the missing PARAMETER is the client's mistake.
1478
1560
  if (!userid) {
1479
- sendJson(res, 400, { ok: false, error: "userid required" });
1561
+ if (qname) {
1562
+ sendJson(res, 200, { ok: true, registered: false, profile: null });
1563
+ return;
1564
+ }
1565
+ sendJson(res, 400, { ok: false, error: "userid or name required" });
1480
1566
  return;
1481
1567
  }
1482
1568
  // A 52-char Carrier ADDRESS also works (group cards only have that):
@@ -1596,6 +1682,10 @@ export function startBeagleServer(opts) {
1596
1682
  agent: false,
1597
1683
  lastMsg: lm ? (lm.dir === "out" ? "you: " : "") + (lm.text ?? "") : "",
1598
1684
  lastTime: fmtTime(lm?.ts),
1685
+ // The raw stamp too: lastTime is formatted for display and cannot
1686
+ // be ordered, which is why the sidebar never reordered on new
1687
+ // activity.
1688
+ lastTs: lm?.ts ?? 0,
1599
1689
  wire: "163",
1600
1690
  avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
1601
1691
  punkId: ensPub.get(uid)?.punkId ?? null,
@@ -1606,8 +1696,13 @@ export function startBeagleServer(opts) {
1606
1696
  id: p.userid || p.address || `r${i}`,
1607
1697
  carrier: p.address || p.userid || "",
1608
1698
  userid: p.userid || "",
1609
- via: "lan",
1610
- time: "",
1699
+ // Name and introduction travel in the friend-request packet and were
1700
+ // being dropped here, so every request looked identical and there
1701
+ // was nothing to decide on.
1702
+ name: p.name || "",
1703
+ descr: p.descr || "",
1704
+ via: p.hello || "lan",
1705
+ time: fmtTime(p.arrivedAt),
1611
1706
  }));
1612
1707
  // Exit nodes = the AVAILABLE exits this node knows about, grouped by
1613
1708
  // region. The authoritative list is the shipped DEFAULT_EXITS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,7 @@
19
19
  }
20
20
  },
21
21
  "scripts": {
22
- "build": "tsc -p tsconfig.json && chmod +x dist/cli.js && node scripts/build-ui.mjs",
22
+ "build": "npm run build -w @decentnetwork/beagle-ui && tsc -p tsconfig.json && chmod +x dist/cli.js && node scripts/build-ui.mjs",
23
23
  "typecheck": "tsc --noEmit",
24
24
  "clean": "rm -rf dist",
25
25
  "start": "node dist/cli.js"
@@ -33,6 +33,7 @@
33
33
  "yargs": "^17.7.2"
34
34
  },
35
35
  "devDependencies": {
36
+ "@decentnetwork/beagle-ui": "0.1.1",
36
37
  "@types/js-yaml": "^4.0.9",
37
38
  "@types/node": "^20.11.0",
38
39
  "@types/yargs": "^17.0.32",
@@ -42,5 +43,8 @@
42
43
  "engines": {
43
44
  "node": ">=20"
44
45
  },
45
- "license": "MIT"
46
+ "license": "MIT",
47
+ "workspaces": [
48
+ "packages/*"
49
+ ]
46
50
  }