@decentnetwork/beagle 0.1.61 → 0.1.64

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.
@@ -18,6 +18,9 @@ export interface EmbeddedHostOptions {
18
18
  }[];
19
19
  nickname?: string;
20
20
  statusMessage?: string;
21
+ /** Our CryptoPunks id, advertised to friends in the userinfo `gender`
22
+ * field — the only avatar path that does not need a server. */
23
+ punkId?: number;
21
24
  autoAcceptFriends: boolean;
22
25
  onProfileChange?: (info: {
23
26
  name?: string;
@@ -138,6 +138,9 @@ export class EmbeddedHost {
138
138
  expressNodes: this.#opts.expressNodes,
139
139
  nickname: this.#opts.nickname,
140
140
  statusMessage: this.#opts.statusMessage,
141
+ // Advertised from startup, not only after a profile edit — otherwise a
142
+ // restart silently stops telling friends what our avatar is.
143
+ punkId: this.#opts.punkId,
141
144
  fileResumeDir: resolve(this.#opts.configDir, "file-resume"),
142
145
  });
143
146
  this.#wire();
@@ -402,7 +405,9 @@ export class EmbeddedHost {
402
405
  const id = this.#node.identity();
403
406
  return {
404
407
  identity: id,
405
- node: { name: this.#opts.nickname ?? "", backend: "embedded", autoAccept: this.#autoAccept },
408
+ // punkId so the UI can report what is actually being advertised,
409
+ // which is not necessarily what beagles.eth says.
410
+ node: { name: this.#opts.nickname ?? "", backend: "embedded", autoAccept: this.#autoAccept, punkId: this.#opts.punkId ?? null },
406
411
  // No TUN in this backend — say so explicitly rather than omitting
407
412
  // the key, so the UI can distinguish "no LAN" from "unknown".
408
413
  tun: null,
@@ -448,6 +453,10 @@ export class EmbeddedHost {
448
453
  f.status === "online" ||
449
454
  this.#messages.hasInbound(f.carrierId),
450
455
  lastSeen: f.acceptedAt,
456
+ // The avatar they advertised over Carrier (userinfo `gender`).
457
+ // Peer-to-peer, so it works for a friend who never registered a
458
+ // beagles.eth name — until now that meant an identicon.
459
+ punkId: f.punkId ?? null,
451
460
  pinned: meta?.pinned ?? false,
452
461
  lastMessage: lastMsg ? { dir: lastMsg.dir, text: lastMsg.text, ts: lastMsg.ts } : undefined,
453
462
  unread: this.#messages.unreadCount(f.carrierId, meta?.lastReadTs ?? 0),
@@ -511,12 +520,21 @@ export class EmbeddedHost {
511
520
  this.#meta.setAlias(uid, req.alias);
512
521
  return {};
513
522
  case "set-profile": {
514
- const info = { name: req.name, description: req.description };
523
+ const info = {
524
+ name: req.name,
525
+ description: req.description,
526
+ // null clears, undefined leaves alone. setUserInfo re-sends the
527
+ // profile to every established friend, so a new avatar lands now
528
+ // rather than on their next reconnect.
529
+ punkId: req.punkId === undefined ? undefined : (req.punkId === null ? null : Number(req.punkId)),
530
+ };
515
531
  this.#node.setUserInfo(info);
516
532
  if (info.name)
517
533
  this.#opts.nickname = info.name;
518
534
  if (info.description !== undefined)
519
535
  this.#opts.statusMessage = info.description;
536
+ if (info.punkId !== undefined)
537
+ this.#opts.punkId = info.punkId ?? undefined;
520
538
  this.#opts.onProfileChange?.(info);
521
539
  return {};
522
540
  }
package/dist/server.js CHANGED
@@ -273,7 +273,14 @@ function installMode() {
273
273
  return "npx";
274
274
  if (BEAGLE_ROOT.includes(join("node_modules", "@decentnetwork", "beagle")))
275
275
  return "global";
276
- return "dev";
276
+ // "dev" has to be PROVEN, not assumed. It used to be the fallthrough for
277
+ // any layout this function did not recognise, so a user with a perfectly
278
+ // ordinary install was told to run `git pull` — against a private repo they
279
+ // cannot clone, with no other option offered. A dead end presented as
280
+ // instructions.
281
+ if (existsSync(join(BEAGLE_ROOT, ".git")))
282
+ return "dev";
283
+ return "unknown";
277
284
  }
278
285
  async function npmLatest(pkg) {
279
286
  const ctl = new AbortController();
@@ -660,6 +667,10 @@ const fmtTime = (ts) => {
660
667
  return `${d.getDate()} ${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][d.getMonth()]}`;
661
668
  };
662
669
  const viaFromTransport = (t) => t === "udp" || t === "both" ? "direct" : t === "tcp-relay" ? "relay" : null;
670
+ // What we last told the daemon to advertise. Module-scoped on purpose: the
671
+ // value is per-process, and re-sending it on every /api/desktop poll would
672
+ // re-push the profile to every friend several times a second.
673
+ let lastAdvertisedPunk;
663
674
  export function startBeagleServer(opts) {
664
675
  const host = opts.listenHost ?? "127.0.0.1";
665
676
  const port = opts.listenPort ?? 8765;
@@ -685,6 +696,32 @@ export function startBeagleServer(opts) {
685
696
  const handler = async (req, res) => {
686
697
  try {
687
698
  const url = (req.url || "/").split("?")[0];
699
+ // Let Sign-in-with-Beagle pages (meet, billing, …) detect this daemon
700
+ // from a public https origin. Chrome Local Network Access blocks the
701
+ // probe unless we answer the private-network preflight.
702
+ const detect = url === "/api/state" || url === "/api/ping" || url === "/connect";
703
+ if (detect) {
704
+ const origin = String(req.headers.origin || "");
705
+ if (origin) {
706
+ res.setHeader("Access-Control-Allow-Origin", origin);
707
+ res.setHeader("Vary", "Origin");
708
+ }
709
+ else {
710
+ res.setHeader("Access-Control-Allow-Origin", "*");
711
+ }
712
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
713
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
714
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
715
+ if (req.method === "OPTIONS") {
716
+ res.writeHead(204);
717
+ res.end();
718
+ return;
719
+ }
720
+ }
721
+ if (req.method === "GET" && url === "/api/ping") {
722
+ sendJson(res, 200, { ok: true });
723
+ return;
724
+ }
688
725
  // Desktop UI bundle (the design). Falls back to the classic page when
689
726
  // the bundle hasn't been built (dist/ui/desktop missing).
690
727
  const desktopIndex = join(DESKTOP_DIR, "index.html");
@@ -739,7 +776,7 @@ export function startBeagleServer(opts) {
739
776
  // LOCALHOST-ONLY: the popup always runs in the local user's browser
740
777
  // (localhost), so binding the UI to a LAN IP must never let a remote
741
778
  // host request signatures with this identity.
742
- if (url === "/connect" || url === "/api/connect-approve") {
779
+ if (url === "/connect" || url === "/api/connect-approve" || url === "/api/launch-token") {
743
780
  if (!isLocalRequest(req)) {
744
781
  res.writeHead(403, { "content-type": "text/plain" });
745
782
  res.end("Sign in with Beagle is available only on this machine (localhost).");
@@ -804,6 +841,42 @@ export function startBeagleServer(opts) {
804
841
  sendJson(res, 200, { ok: true, userid, address, sig: r.data?.sig ?? "", name, avatar, punkId });
805
842
  return;
806
843
  }
844
+ // Mint the "launch an app as me" assertion the Apps tab puts in an app's
845
+ // URL fragment, so someone opening an app from their own client is not
846
+ // asked to sign in to it. LOCALHOST-ONLY like /connect: it is a
847
+ // signature with this identity.
848
+ if (req.method === "POST" && url === "/api/launch-token") {
849
+ const body = await readBody(req);
850
+ const target = validateOrigin(body.origin);
851
+ if (!target) {
852
+ sendJson(res, 400, { ok: false, error: "invalid origin" });
853
+ return;
854
+ }
855
+ const ts = Date.now();
856
+ // A DIFFERENT prefix from decent-auth on purpose: an assertion minted
857
+ // here must never be replayable as a "a site asked me to sign in"
858
+ // proof, nor the reverse.
859
+ const signed = await opts.call({ op: "sign", text: `decent-launch\n${target}\n${ts}` });
860
+ if (!signed.ok) {
861
+ sendJson(res, 502, { ok: false, error: signed.error || "sign failed" });
862
+ return;
863
+ }
864
+ const diag = await opts.call({ op: "diag" });
865
+ const d = (diag.ok ? diag.data : {}) ?? {};
866
+ const identity = d.identity ?? {};
867
+ const node = d.node ?? {};
868
+ sendJson(res, 200, {
869
+ ok: true,
870
+ v: 1,
871
+ userid: signed.data?.userid ?? identity.userid ?? "",
872
+ address: identity.address ?? "",
873
+ name: node.name ?? "",
874
+ punkId: null,
875
+ ts,
876
+ sig: signed.data?.sig ?? "",
877
+ });
878
+ return;
879
+ }
807
880
  // ── beagles.eth name registration ──
808
881
  // Register/update this node's *.beagles.eth name and bind wallet
809
882
  // addresses into the record. The write routes trigger identity
@@ -1530,6 +1603,11 @@ export function startBeagleServer(opts) {
1530
1603
  ok: true,
1531
1604
  checkedAt: Date.now(),
1532
1605
  mode: installMode(),
1606
+ // WHO owns the peer version. With the daemon backend it is the
1607
+ // agentnet/lan process, not this app — so "update beagle" cannot
1608
+ // move that row, and telling someone to rebuild beagle (or pull the
1609
+ // peer repo) is the wrong remedy for it. The lan update is.
1610
+ peerOwner: (opts.lanHost?.kind ?? "embedded") === "daemon" ? "daemon" : "embedded",
1533
1611
  updates: [
1534
1612
  row(BEAGLE_PKG, beagleVersion(), beagleLatest),
1535
1613
  // The peer the BACKEND is running — a daemon that has been updated
@@ -1567,6 +1645,10 @@ export function startBeagleServer(opts) {
1567
1645
  sendJson(res, 400, { ok: false, error: "running from a source checkout — update with: git pull && npm run build" });
1568
1646
  return;
1569
1647
  }
1648
+ // Layout we do not recognise: try the thing that works for almost
1649
+ // everyone rather than refusing. If it genuinely cannot install, the
1650
+ // npm error says so — which is still more use than a git command
1651
+ // against a repo the user has no access to.
1570
1652
  if (mode === "npx") {
1571
1653
  // npx with @latest re-resolves on every launch: restarting IS the update.
1572
1654
  sendJson(res, 200, { ok: true, npx: true });
@@ -1644,6 +1726,20 @@ export function startBeagleServer(opts) {
1644
1726
  ]);
1645
1727
  const d = (diag.ok ? diag.data : {}) ?? {};
1646
1728
  const identity = d.identity ?? {};
1729
+ // A desktop install has no locally-picked punk: its avatar is whatever
1730
+ // it registered on beagles.eth. Advertise THAT over Carrier, so an iOS
1731
+ // friend sees a picture instead of nothing. Sent only when the value
1732
+ // CHANGES — set-profile re-pushes the profile to every established
1733
+ // friend, and doing that on every poll would be a packet storm.
1734
+ const myPunk = ensPub.get(identity.userid ?? "")?.punkId ?? null;
1735
+ // Only ever PUSH a real punk, never a null. A null here means "nothing
1736
+ // registered on beagles.eth", which is not the same as "no avatar" —
1737
+ // pushing it wiped a punk set directly through /api/set-profile, on
1738
+ // the very next poll. Registration seeds the avatar; it does not own it.
1739
+ if (myPunk != null && myPunk !== lastAdvertisedPunk) {
1740
+ lastAdvertisedPunk = myPunk;
1741
+ void opts.call({ op: "set-profile", punkId: myPunk }).catch(() => undefined);
1742
+ }
1647
1743
  const tun = d.tun ?? {};
1648
1744
  const node = d.node ?? {};
1649
1745
  const diagFriends = d.friends ?? [];
@@ -1699,7 +1795,7 @@ export function startBeagleServer(opts) {
1699
1795
  // toggle rather than showing a state it cannot know.
1700
1796
  autoAccept: typeof node.autoAccept === "boolean" ? node.autoAccept : null,
1701
1797
  avatarUrl: ensPub.get(identity.userid ?? "")?.avatarUrl ?? null,
1702
- punkId: ensPub.get(identity.userid ?? "")?.punkId ?? null,
1798
+ punkId: myPunk ?? node.punkId ?? null,
1703
1799
  // The UI compares this against its baked-in __DK_UI_VERSION and
1704
1800
  // reloads itself when they differ — an open tab otherwise runs a
1705
1801
  // stale bundle forever after a beagle update (multi-tab call fixes
@@ -1740,7 +1836,11 @@ export function startBeagleServer(opts) {
1740
1836
  lastTs: lm?.ts ?? 0,
1741
1837
  wire: "163",
1742
1838
  avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
1743
- punkId: ensPub.get(uid)?.punkId ?? null,
1839
+ // What they advertised over Carrier wins over the directory: it is
1840
+ // what they are using on their device right now, it needs no
1841
+ // gateway, and it is the only avatar a friend who never registered
1842
+ // a beagles.eth name will ever have.
1843
+ punkId: f.punkId ?? ensPub.get(uid)?.punkId ?? null,
1744
1844
  };
1745
1845
  });
1746
1846
  const pend = pending.ok ? (pending.data?.pending ?? []) : [];
@@ -1973,8 +2073,10 @@ export function startBeagleServer(opts) {
1973
2073
  return;
1974
2074
  }
1975
2075
  if (req.method === "POST" && url === "/api/set-profile") {
1976
- const { name, description } = await readBody(req);
1977
- const r = await opts.call({ op: "set-profile", name, description });
2076
+ // punkId passes through so an avatar can be set without a beagles.eth
2077
+ // registration the daemon advertises it over Carrier either way.
2078
+ const { name, description, punkId } = await readBody(req);
2079
+ const r = await opts.call({ op: "set-profile", name, description, punkId });
1978
2080
  sendJson(res, r.ok ? 200 : 400, r);
1979
2081
  return;
1980
2082
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.61",
3
+ "version": "0.1.64",
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",
@@ -27,13 +27,13 @@
27
27
  "dependencies": {
28
28
  "@decentnetwork/chat-components": "^0.1.3",
29
29
  "@decentnetwork/lan": "^0.1.283",
30
- "@decentnetwork/peer": "^0.1.141",
30
+ "@decentnetwork/peer": "0.1.143",
31
31
  "@decentnetwork/peer-webrtc": "^0.2.16",
32
32
  "js-yaml": "^4.1.0",
33
33
  "yargs": "^17.7.2"
34
34
  },
35
35
  "devDependencies": {
36
- "@decentnetwork/beagle-ui": "0.1.2",
36
+ "@decentnetwork/beagle-ui": "0.2.0",
37
37
  "@types/js-yaml": "^4.0.9",
38
38
  "@types/node": "^20.11.0",
39
39
  "@types/yargs": "^17.0.32",