@decentnetwork/beagle 0.1.57 → 0.1.59

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
  });
@@ -46,6 +46,8 @@ export interface OpenPeerHostOptions {
46
46
  /** Force a backend instead of auto-detecting. Mainly for testing the
47
47
  * embedded path on a machine that also runs a daemon. */
48
48
  force?: BackendKind;
49
+ /** How long `force: "daemon"` waits for the daemon socket. Default 60s. */
50
+ waitForDaemonMs?: number;
49
51
  }
50
52
  /**
51
53
  * Pick a backend.
package/dist/peer-host.js CHANGED
@@ -77,7 +77,35 @@ export async function openPeerHost(opts) {
77
77
  return { host: new DaemonHost(dataDir), why: `decentlan daemon at ${dataDir}` };
78
78
  }
79
79
  if (opts.force === "daemon") {
80
- throw new Error(`--backend daemon was requested but no decentlan daemon is running (looked for ${ipcSocketPath(dataDir)}).`);
80
+ // Wait rather than die.
81
+ //
82
+ // A supervised beagle (pm2, systemd) routinely starts BEFORE the daemon it
83
+ // is meant to attach to. Throwing there turned a boot-order race into a
84
+ // permanent problem: beagle fell back to embedded, took <dataDir>/daemon.pid,
85
+ // and from then on `agentnet service restart` refused to start the daemon
86
+ // because a live process already held the identity — which is why upgrading
87
+ // needed a pm2 stop / restart / pm2 start dance instead of one command.
88
+ //
89
+ // Explicit --backend daemon means "this box's identity belongs to the
90
+ // daemon". Honour that by waiting for it to appear.
91
+ const waitMs = Math.max(0, opts.waitForDaemonMs ?? 60_000);
92
+ const deadline = Date.now() + waitMs;
93
+ let waited = false;
94
+ while (!daemonIsRunning(dataDir)) {
95
+ if (Date.now() >= deadline) {
96
+ throw new Error(`--backend daemon was requested but no decentlan daemon appeared within ${Math.round(waitMs / 1000)}s ` +
97
+ `(looked for ${ipcSocketPath(dataDir)}).`);
98
+ }
99
+ if (!waited) {
100
+ waited = true;
101
+ console.log(`waiting up to ${Math.round(waitMs / 1000)}s for the decentlan daemon at ${ipcSocketPath(dataDir)}…`);
102
+ }
103
+ await new Promise((r) => setTimeout(r, 1000));
104
+ }
105
+ return {
106
+ host: new DaemonHost(dataDir),
107
+ why: waited ? `decentlan daemon at ${dataDir} (after waiting)` : `decentlan daemon at ${dataDir}`,
108
+ };
81
109
  }
82
110
  // Reuse decentlan's identity when it exists so friends carry over — this is
83
111
  // the same node, not a new one. The daemon is not running, so nothing else
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");
@@ -760,7 +761,13 @@ export function startBeagleServer(opts) {
760
761
  sendJson(res, 200, { ok: true, punk });
761
762
  }
762
763
  catch (err) {
763
- sendJson(res, 502, { ok: false, error: String(err?.message ?? err) });
764
+ // An id the set does not contain is a normal negative answer, not a
765
+ // gateway failure. Reporting 502 made a bad stored punkId look like
766
+ // the punks service was down, and the client — which only cached
767
+ // successes — retried it on every render.
768
+ const msg = String(err?.message ?? err);
769
+ const missing = /\b404\b|not found/i.test(msg);
770
+ sendJson(res, missing ? 404 : 502, { ok: false, error: missing ? "no such punk" : msg });
764
771
  }
765
772
  return;
766
773
  }
@@ -1152,6 +1159,9 @@ export function startBeagleServer(opts) {
1152
1159
  }
1153
1160
  const q = new URL(req.url || "", "http://x").searchParams;
1154
1161
  const userid = q.get("userid") || "";
1162
+ // The sender's own copy needs saving too, or its chip renders as an
1163
+ // empty 0 B card: the bytes went peer-to-peer and never touched here.
1164
+ const dir = q.get("dir") === "out" ? "out" : "in";
1155
1165
  const rawName = q.get("name") || "agentnet-file";
1156
1166
  const safe = rawName.replace(/[/\\]/g, "_").slice(0, 200) || "agentnet-file";
1157
1167
  if (!userid) {
@@ -1178,7 +1188,7 @@ export function startBeagleServer(opts) {
1178
1188
  ws.on("finish", () => resolve2());
1179
1189
  });
1180
1190
  const st = statSync(finalPath);
1181
- const r = await opts.call({ op: "file-log-local", userid, dir: "in", name: finalName, size: st.size });
1191
+ const r = await opts.call({ op: "file-log-local", userid, dir, name: finalName, size: st.size });
1182
1192
  if (!r.ok) {
1183
1193
  sendJson(res, 502, r);
1184
1194
  return;
@@ -1359,6 +1369,32 @@ export function startBeagleServer(opts) {
1359
1369
  }
1360
1370
  return;
1361
1371
  }
1372
+ if (req.method === "GET" && url === "/api/bridge-roster") {
1373
+ // Same shape the browser build returns, so discover.jsx renders it
1374
+ // without knowing which client it is running in.
1375
+ if (!opts.bridgeWs || !opts.callIce?.keyFile) {
1376
+ return sendJson(res, 200, { ok: true, list: [], pending: true });
1377
+ }
1378
+ try {
1379
+ // diag already carries both facts, so there is no second round trip
1380
+ // and no op to add: the address is the identity, the nickname is the
1381
+ // node's own name.
1382
+ const diag = (await opts.call({ op: "diag" }));
1383
+ const r = await bridgeRoster(opts.bridgeWs, opts.callIce.keyFile, {
1384
+ name: diag?.node?.name,
1385
+ address: diag?.identity?.address,
1386
+ });
1387
+ if (!r)
1388
+ return sendJson(res, 200, { ok: true, list: [], pending: true });
1389
+ return sendJson(res, 200, {
1390
+ ok: true, bridge: r.bridge, online: r.online,
1391
+ list: r.peers.map((p) => ({ ...p, punkId: p.punk ?? null })),
1392
+ });
1393
+ }
1394
+ catch (err) {
1395
+ return sendJson(res, 200, { ok: false, error: String(err?.message ?? err) });
1396
+ }
1397
+ }
1362
1398
  if (req.method === "GET" && url === "/api/discover-registered") {
1363
1399
  try {
1364
1400
  const raw = (await fetchDiscoverJson(`${ENS_GATEWAY}/names`));
@@ -1475,8 +1511,28 @@ export function startBeagleServer(opts) {
1475
1511
  if (req.method === "GET" && url === "/api/ens-profile") {
1476
1512
  const q = new URL(req.url || "", "http://localhost").searchParams;
1477
1513
  let userid = q.get("userid") || "";
1514
+ // Also accept ?name= (a beagles.eth display name), so a group sender
1515
+ // known only by nickname still resolves to its avatar — the userid is
1516
+ // simply not available for those.
1517
+ const qname = q.get("name") || "";
1518
+ if (!userid && qname) {
1519
+ const norm = (x) => String(x || "").toLowerCase().replace(/\s+/g, "");
1520
+ for (const [uid, prof] of await ensPublicByUserid(1500)) {
1521
+ if (norm(prof.displayName || "") === norm(qname) || norm(prof.ens || "") === norm(qname)) {
1522
+ userid = uid;
1523
+ break;
1524
+ }
1525
+ }
1526
+ }
1527
+ // A name nobody has registered is a lookup that found nothing, not a
1528
+ // bad request: 400 makes the caller log an error and, worse, retry.
1529
+ // Only the missing PARAMETER is the client's mistake.
1478
1530
  if (!userid) {
1479
- sendJson(res, 400, { ok: false, error: "userid required" });
1531
+ if (qname) {
1532
+ sendJson(res, 200, { ok: true, registered: false, profile: null });
1533
+ return;
1534
+ }
1535
+ sendJson(res, 400, { ok: false, error: "userid or name required" });
1480
1536
  return;
1481
1537
  }
1482
1538
  // A 52-char Carrier ADDRESS also works (group cards only have that):
@@ -1596,6 +1652,10 @@ export function startBeagleServer(opts) {
1596
1652
  agent: false,
1597
1653
  lastMsg: lm ? (lm.dir === "out" ? "you: " : "") + (lm.text ?? "") : "",
1598
1654
  lastTime: fmtTime(lm?.ts),
1655
+ // The raw stamp too: lastTime is formatted for display and cannot
1656
+ // be ordered, which is why the sidebar never reordered on new
1657
+ // activity.
1658
+ lastTs: lm?.ts ?? 0,
1599
1659
  wire: "163",
1600
1660
  avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
1601
1661
  punkId: ensPub.get(uid)?.punkId ?? null,
@@ -1606,8 +1666,13 @@ export function startBeagleServer(opts) {
1606
1666
  id: p.userid || p.address || `r${i}`,
1607
1667
  carrier: p.address || p.userid || "",
1608
1668
  userid: p.userid || "",
1609
- via: "lan",
1610
- time: "",
1669
+ // Name and introduction travel in the friend-request packet and were
1670
+ // being dropped here, so every request looked identical and there
1671
+ // was nothing to decide on.
1672
+ name: p.name || "",
1673
+ descr: p.descr || "",
1674
+ via: p.hello || "lan",
1675
+ time: fmtTime(p.arrivedAt),
1611
1676
  }));
1612
1677
  // Exit nodes = the AVAILABLE exits this node knows about, grouped by
1613
1678
  // 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.57",
3
+ "version": "0.1.59",
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.0",
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
  }