@decentnetwork/beagle 0.1.122 → 0.1.124

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.
@@ -97,6 +97,15 @@ export declare class CarrierNode extends EventEmitter {
97
97
  force?: boolean;
98
98
  }): Promise<void>;
99
99
  acceptFriendRequest(pubkey: string): Promise<void>;
100
+ /** The SDK's pending inbound requests (empty before create or on an SDK
101
+ * without the call). Re-read when answering friends-pending: a late
102
+ * profile patches the SDK's entry without firing a new event. */
103
+ pendingFriendRequests(): Array<{
104
+ pubkey: string;
105
+ userid?: string;
106
+ punkId?: number;
107
+ avatarUrl?: string;
108
+ }>;
100
109
  removeFriend(userid: string): boolean;
101
110
  /** Decline an inbound request and clear what it created in the SDK. */
102
111
  rejectFriendRequest(pubkey: string): void;
@@ -186,6 +186,13 @@ export class CarrierNode extends EventEmitter {
186
186
  acceptFriendRequest(pubkey) {
187
187
  return this.#require().acceptFriendRequest(pubkey);
188
188
  }
189
+ /** The SDK's pending inbound requests (empty before create or on an SDK
190
+ * without the call). Re-read when answering friends-pending: a late
191
+ * profile patches the SDK's entry without firing a new event. */
192
+ pendingFriendRequests() {
193
+ const list = this.#peer?.pendingFriendRequests?.();
194
+ return Array.isArray(list) ? list : [];
195
+ }
189
196
  removeFriend(userid) {
190
197
  return this.#require().removeFriend(userid);
191
198
  }
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.122";
1
+ window.__DK_UI_VERSION="0.1.124";
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/beagle-ui/dist/beagle-ui.js
@@ -1697,6 +1697,37 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1697
1697
  }, [append, lang, onReloadFriends]);
1698
1698
  return { thread, handle, removeFriends, adoptServicePeers };
1699
1699
  }
1700
+ var REQUEST_AVATAR_HOST = "ens-gateway.beaglechat.workers.dev";
1701
+ var MAX_URL = 2048;
1702
+ function requestAvatarUrl(r) {
1703
+ const u = r && r.avatarUrl;
1704
+ if (typeof u !== "string" || !u || u.length > MAX_URL)
1705
+ return null;
1706
+ let parsed;
1707
+ try {
1708
+ parsed = new URL(u);
1709
+ } catch {
1710
+ return null;
1711
+ }
1712
+ if (parsed.protocol !== "https:" || parsed.host !== REQUEST_AVATAR_HOST)
1713
+ return null;
1714
+ if (parsed.username || parsed.password)
1715
+ return null;
1716
+ return parsed.href;
1717
+ }
1718
+ function requestPunkId(r) {
1719
+ const p = r && r.punkId;
1720
+ return typeof p === "number" && Number.isInteger(p) && p >= 0 ? p : null;
1721
+ }
1722
+ function requestAvatarSource(r, skipUrl = false) {
1723
+ const url = skipUrl ? null : requestAvatarUrl(r);
1724
+ if (url)
1725
+ return { kind: "url", url };
1726
+ const id = requestPunkId(r);
1727
+ if (id != null)
1728
+ return { kind: "punk", id };
1729
+ return { kind: "ens" };
1730
+ }
1700
1731
  var DK_CMD_HIST_KEY = "beagle.assistant.history";
1701
1732
  var DK_CMD_HIST_MAX = 50;
1702
1733
  function RequestsBlock({ T, requests, onAct }) {
@@ -1740,7 +1771,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1740
1771
  display: "flex",
1741
1772
  flexDirection: "column",
1742
1773
  gap: 8
1743
- } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkEnsAvatar, { userid: r.userid || r.id, name: r.name, fallbackSeed: r.carrier, size: 30, radius: 7 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, r.name ? /* @__PURE__ */ React.createElement("div", { style: {
1774
+ } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkRequestAvatar, { r, size: 30, radius: 7 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, r.name ? /* @__PURE__ */ React.createElement("div", { style: {
1744
1775
  fontFamily: "var(--ui)",
1745
1776
  fontSize: 13,
1746
1777
  fontWeight: 600,
@@ -1757,6 +1788,32 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1757
1788
  paddingLeft: 8
1758
1789
  } }, r.descr), failed[r.id] && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--danger)" } }, failed[r.id]), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => act(r, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => act(r, "reject"), style: { flex: 1 } }, T.reject))))));
1759
1790
  }
1791
+ function DkRequestAvatar({ r, size, radius }) {
1792
+ const [urlBroken, setUrlBroken] = React.useState(false);
1793
+ const url = requestAvatarUrl(r);
1794
+ React.useEffect(() => {
1795
+ setUrlBroken(false);
1796
+ }, [url]);
1797
+ const src = requestAvatarSource(r, urlBroken);
1798
+ const seed = r.carrier || r.userid || r.id;
1799
+ if (src.kind === "url") {
1800
+ return /* @__PURE__ */ React.createElement(
1801
+ "img",
1802
+ {
1803
+ src: src.url,
1804
+ alt: "",
1805
+ width: size,
1806
+ height: size,
1807
+ referrerPolicy: "no-referrer",
1808
+ onError: () => setUrlBroken(true),
1809
+ style: { width: size, height: size, borderRadius: radius, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" }
1810
+ }
1811
+ );
1812
+ }
1813
+ if (src.kind === "punk")
1814
+ return /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: src.id, size, radius, fallbackSeed: seed });
1815
+ return /* @__PURE__ */ React.createElement(DkEnsAvatar, { userid: r.userid || r.id, name: r.name, fallbackSeed: r.carrier, size, radius });
1816
+ }
1760
1817
  function PeerRow({ peer, T, active, onClick, muted, isProtocol, fix }) {
1761
1818
  const name = peer.alias || peer.userId;
1762
1819
  const row = rosterRow(peer, isProtocol || (() => false), fix);
@@ -18,6 +18,7 @@ import { MessageStore } from "./message-store.js";
18
18
  import { FriendMetaStore } from "./friend-meta.js";
19
19
  import { Logger } from "./logger.js";
20
20
  import { acquireIdentityLock } from "./identity-lock.js";
21
+ import { refreshPendingAvatars, requesterAvatar } from "./pending-requests.js";
21
22
  /** Files bigger than this are not queued for an offline friend — the bytes sit
22
23
  * on disk until they reconnect, and an unbounded queue is how you fill a
23
24
  * user's disk by accident. Live transfers have no such limit. */
@@ -228,6 +229,9 @@ export class EmbeddedHost {
228
229
  name: req.name,
229
230
  descr: req.description,
230
231
  hello: req.hello,
232
+ // The requester's punk / avatar URL (peer >= 0.1.170), so the card can
233
+ // draw it before accept. A later profile is picked up in friends-pending.
234
+ ...requesterAvatar(req),
231
235
  arrivedAt: Date.now(),
232
236
  });
233
237
  this.#events.emit("event", { type: "friend-request", userid });
@@ -493,6 +497,9 @@ export class EmbeddedHost {
493
497
  return {};
494
498
  }
495
499
  case "friends-pending":
500
+ // Re-read the SDK's list: a requester's profile that arrived after its
501
+ // request patches only the SDK's entry, with no new event.
502
+ refreshPendingAvatars(this.#pending.values(), this.#node.pendingFriendRequests());
496
503
  return { pending: [...this.#pending.values()] };
497
504
  case "friends-accept": {
498
505
  const entry = this.#pending.get(uid);
@@ -0,0 +1,28 @@
1
+ export interface RequesterAvatar {
2
+ punkId?: number;
3
+ avatarUrl?: string;
4
+ }
5
+ /** The valid avatar fields of an SDK / backend friend-request record. Keys are
6
+ * present only with a value; punk 0 is a real CryptoPunk. */
7
+ export declare function requesterAvatar(src: {
8
+ punkId?: unknown;
9
+ avatarUrl?: unknown;
10
+ } | undefined | null): RequesterAvatar;
11
+ /** Fold the SDK's current pending list into the host's own entries, matched by
12
+ * pubkey or userid. A value the SDK has wins (a late profile can change it); a
13
+ * value only the host has is kept. Mutates `entries`; returns whether anything
14
+ * changed. */
15
+ export declare function refreshPendingAvatars<E extends {
16
+ userid: string;
17
+ pubkey: string;
18
+ } & RequesterAvatar>(entries: Iterable<E>, live: ReadonlyArray<{
19
+ pubkey?: string;
20
+ userid?: string;
21
+ punkId?: unknown;
22
+ avatarUrl?: unknown;
23
+ }>): boolean;
24
+ /** One `/api/desktop` `requests[]` row from a friends-pending entry (daemon or
25
+ * embedded). `punkId` / `avatarUrl` are null when the requester sent none —
26
+ * the same convention as `peers[]` — and the card then falls back to the
27
+ * beagles.eth lookup and a generated picture. */
28
+ export declare function desktopRequestRow(p: Record<string, unknown>, i: number, fmtTime: (ts?: number) => string): Record<string, unknown>;
@@ -0,0 +1,69 @@
1
+ // The requester's avatar on a pending friend request, before anyone accepts.
2
+ //
3
+ // peer >= 0.1.170 carries the requester's punk (friendreq field 3, `gender`)
4
+ // and custom avatar URL (field 4) in the request itself, and fills them from a
5
+ // profile the requester pushed before accept. When that profile arrives after
6
+ // the request, the SDK patches only its own pending list — no new event — so
7
+ // a host that copied the event once must re-read peer.pendingFriendRequests()
8
+ // to learn it. lan >= 0.1.313 does that for a daemon backend; the embedded host
9
+ // does it here. See decent-hq docs/tasks/TASK-2026-11-01-avatar-on-friend-request.md.
10
+ import { punkIdOf } from "./friend-store.js";
11
+ /** The valid avatar fields of an SDK / backend friend-request record. Keys are
12
+ * present only with a value; punk 0 is a real CryptoPunk. */
13
+ export function requesterAvatar(src) {
14
+ const out = {};
15
+ if (!src)
16
+ return out;
17
+ const punk = punkIdOf(src.punkId);
18
+ if (punk !== undefined && Number.isInteger(punk))
19
+ out.punkId = punk;
20
+ if (typeof src.avatarUrl === "string" && src.avatarUrl.length > 0)
21
+ out.avatarUrl = src.avatarUrl;
22
+ return out;
23
+ }
24
+ /** Fold the SDK's current pending list into the host's own entries, matched by
25
+ * pubkey or userid. A value the SDK has wins (a late profile can change it); a
26
+ * value only the host has is kept. Mutates `entries`; returns whether anything
27
+ * changed. */
28
+ export function refreshPendingAvatars(entries, live) {
29
+ if (!live.length)
30
+ return false;
31
+ let changed = false;
32
+ for (const e of entries) {
33
+ const l = live.find((x) => (!!x.pubkey && x.pubkey === e.pubkey) || (!!x.userid && x.userid === e.userid));
34
+ if (!l)
35
+ continue;
36
+ const a = requesterAvatar(l);
37
+ if (a.punkId !== undefined && a.punkId !== e.punkId) {
38
+ e.punkId = a.punkId;
39
+ changed = true;
40
+ }
41
+ if (a.avatarUrl !== undefined && a.avatarUrl !== e.avatarUrl) {
42
+ e.avatarUrl = a.avatarUrl;
43
+ changed = true;
44
+ }
45
+ }
46
+ return changed;
47
+ }
48
+ /** One `/api/desktop` `requests[]` row from a friends-pending entry (daemon or
49
+ * embedded). `punkId` / `avatarUrl` are null when the requester sent none —
50
+ * the same convention as `peers[]` — and the card then falls back to the
51
+ * beagles.eth lookup and a generated picture. */
52
+ export function desktopRequestRow(p, i, fmtTime) {
53
+ const a = requesterAvatar(p);
54
+ return {
55
+ id: p.userid || p.address || `r${i}`,
56
+ carrier: p.address || p.userid || "",
57
+ userid: p.userid || "",
58
+ // Name and introduction travel in the friend-request packet and were
59
+ // being dropped here, so every request looked identical and there
60
+ // was nothing to decide on.
61
+ name: p.name || "",
62
+ descr: p.descr || "",
63
+ via: p.hello || "lan",
64
+ time: fmtTime(p.arrivedAt),
65
+ // The requester's avatar, drawn before accept: URL first, then punk.
66
+ punkId: a.punkId ?? null,
67
+ avatarUrl: a.avatarUrl ?? null,
68
+ };
69
+ }
package/dist/server.js CHANGED
@@ -26,6 +26,7 @@ import { bridgeRoster } from "./bridge-roster.js";
26
26
  import { isSupervised } from "./relaunch.js";
27
27
  import { historyStats } from "./assistant-stats.js";
28
28
  import { readFriendStore, pickPunk } from "./friend-store.js";
29
+ import { desktopRequestRow } from "./pending-requests.js";
29
30
  import { createDiscoverFetch } from "./discover-cache.js";
30
31
  import { applyWalletBind } from "@decentnetwork/beagle-ui/wallet-proof";
31
32
  // Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
@@ -2093,18 +2094,9 @@ export function startBeagleServer(opts) {
2093
2094
  };
2094
2095
  });
2095
2096
  const pend = pending.ok ? (pending.data?.pending ?? []) : [];
2096
- const requests = pend.map((p, i) => ({
2097
- id: p.userid || p.address || `r${i}`,
2098
- carrier: p.address || p.userid || "",
2099
- userid: p.userid || "",
2100
- // Name and introduction travel in the friend-request packet and were
2101
- // being dropped here, so every request looked identical and there
2102
- // was nothing to decide on.
2103
- name: p.name || "",
2104
- descr: p.descr || "",
2105
- via: p.hello || "lan",
2106
- time: fmtTime(p.arrivedAt),
2107
- }));
2097
+ // Same rows for both backends: lan >= 0.1.313 and the embedded host
2098
+ // both answer friends-pending with punkId / avatarUrl when known.
2099
+ const requests = pend.map((p, i) => desktopRequestRow(p, i, fmtTime));
2108
2100
  // Exit nodes = the AVAILABLE exits this node knows about, grouped by
2109
2101
  // region. The authoritative list is the shipped DEFAULT_EXITS
2110
2102
  // (config/default-exits.yaml) — every install has it, so the panel is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.122",
3
+ "version": "0.1.124",
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",
@@ -26,10 +26,10 @@
26
26
  "start": "node dist/cli.js"
27
27
  },
28
28
  "dependencies": {
29
- "@decentnetwork/beagle-ui": "^0.2.33",
29
+ "@decentnetwork/beagle-ui": "^0.2.35",
30
30
  "@decentnetwork/chat-components": "^0.1.3",
31
- "@decentnetwork/lan": "^0.1.283",
32
- "@decentnetwork/peer": "^0.1.168",
31
+ "@decentnetwork/lan": "^0.1.313",
32
+ "@decentnetwork/peer": "^0.1.170",
33
33
  "@decentnetwork/peer-webrtc": "^0.2.17",
34
34
  "js-yaml": "^4.1.0",
35
35
  "ws": "^8.21.1",