@decentnetwork/beagle 0.1.23 → 0.1.24

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.
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.22";
1
+ window.__DK_UI_VERSION="0.1.23";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
@@ -627,9 +627,25 @@ function DkBot({ seed, size = 30, radius = 7 }) {
627
627
  justifyContent: "center"
628
628
  } }, /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: size * 0.56, color: "#fff", stroke: 1.9 }));
629
629
  }
630
+ function DkImgAvatar({ url, seed, size, radius }) {
631
+ const [broken, setBroken] = React.useState(false);
632
+ if (broken)
633
+ return /* @__PURE__ */ React.createElement(DkIdenticon, { seed, size, radius });
634
+ return /* @__PURE__ */ React.createElement(
635
+ "img",
636
+ {
637
+ src: url,
638
+ alt: "",
639
+ width: size,
640
+ height: size,
641
+ onError: () => setBroken(true),
642
+ style: { width: size, height: size, borderRadius: radius, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" }
643
+ }
644
+ );
645
+ }
630
646
  function DkAvatar({ peer, size = 30, radius = 7, dot = true }) {
631
647
  const d = Math.max(8, size * 0.3);
632
- return /* @__PURE__ */ React.createElement("div", { style: { position: "relative", width: size, height: size, flexShrink: 0 } }, peer.agent ? /* @__PURE__ */ React.createElement(DkBot, { seed: peer.id, size, radius }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: peer.userId || peer.id, size, radius }), dot && /* @__PURE__ */ React.createElement("span", { style: {
648
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "relative", width: size, height: size, flexShrink: 0 } }, peer.agent ? /* @__PURE__ */ React.createElement(DkBot, { seed: peer.id, size, radius }) : peer.avatarUrl ? /* @__PURE__ */ React.createElement(DkImgAvatar, { url: peer.avatarUrl, seed: peer.userId || peer.id, size, radius }) : peer.punkId != null ? /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: peer.punkId, size, radius, fallbackSeed: peer.userId || peer.id }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: peer.userId || peer.id, size, radius }), dot && /* @__PURE__ */ React.createElement("span", { style: {
633
649
  position: "absolute",
634
650
  right: -2,
635
651
  bottom: -2,
@@ -764,7 +780,7 @@ function Btn({ children, icon, tone = "ghost", onClick, size = "md", title, styl
764
780
  function Section({ label, count, trailing, style }) {
765
781
  return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px", ...style } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "var(--faint)" } }, label), count != null && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "var(--faint)" } }, count), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, height: 1, background: "var(--line)" } }), trailing);
766
782
  }
767
- Object.assign(window, { DkIdenticon, DkBot, DkAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
783
+ Object.assign(window, { DkIdenticon, DkBot, DkAvatar, DkImgAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
768
784
  const __TWEAKS_STYLE = `
769
785
  .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
770
786
  max-height:calc(100vh - 32px);display:flex;flex-direction:column;
@@ -1586,6 +1602,7 @@ function DkGroupSender({ grp, T, avatar }) {
1586
1602
  const [open, setOpen] = React.useState(false);
1587
1603
  const [busy, setBusy] = React.useState(false);
1588
1604
  const [done, setDone] = React.useState(null);
1605
+ const [showProf, setShowProf] = React.useState(false);
1589
1606
  const seed = grp.senderId || grp.sender;
1590
1607
  const addFriend = () => {
1591
1608
  setBusy(true);
@@ -1638,7 +1655,26 @@ function DkGroupSender({ grp, T, avatar }) {
1638
1655
  }
1639
1656
  },
1640
1657
  done || (T && T.add || "Add friend")
1641
- )) : /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", lineHeight: 1.45 } }, T && T.groupSenderUnknown || "The group relays only this display name. Register Beagle as a group agent (/agent add <beagle address> in the group) to receive full member identities you can add as friends."))));
1658
+ ), /* @__PURE__ */ React.createElement(
1659
+ "button",
1660
+ {
1661
+ onClick: () => {
1662
+ setOpen(false);
1663
+ setShowProf(true);
1664
+ },
1665
+ style: {
1666
+ padding: "6px 10px",
1667
+ borderRadius: 8,
1668
+ border: "1px solid var(--line)",
1669
+ background: "transparent",
1670
+ color: "var(--text)",
1671
+ fontFamily: "var(--ui)",
1672
+ fontSize: 12,
1673
+ cursor: "pointer"
1674
+ }
1675
+ },
1676
+ T && T.pubOpen || "View public profile"
1677
+ )) : /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", lineHeight: 1.45 } }, T && T.groupSenderUnknown || "The group relays only this display name. Register Beagle as a group agent (/agent add <beagle address> in the group) to receive full member identities you can add as friends."))), showProf && grp.senderId && /* @__PURE__ */ React.createElement(DkPublicProfile, { T, userid: grp.senderId, fallbackName: grp.sender, onClose: () => setShowProf(false) }));
1642
1678
  }
1643
1679
  function dkThreadIsGroup(thread) {
1644
1680
  return (thread || []).some((m) => !m.day && m.dir !== "out" && !m.file && (dkGroupParse(m.text) || {}).strong);
@@ -2149,6 +2185,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
2149
2185
  const fileRef = React.useRef(null);
2150
2186
  const rtcFileRef = React.useRef(null);
2151
2187
  const [menu, setMenu] = React.useState(false);
2188
+ const [pubProfile, setPubProfile] = React.useState(false);
2152
2189
  const [selMode, setSelMode] = React.useState(false);
2153
2190
  const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
2154
2191
  const [hidden, setHidden] = React.useState(() => /* @__PURE__ */ new Set());
@@ -2397,7 +2434,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
2397
2434
  onDrop
2398
2435
  },
2399
2436
  dragOver && /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 0, zIndex: 30, background: "rgba(91,140,255,0.10)", border: "2px dashed var(--accent)", borderRadius: 12, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", fontFamily: "var(--mono)", fontSize: 14, color: "var(--accent)" } }, lang === "zh" ? "\u677E\u5F00\u53D1\u9001\u6587\u4EF6" : "Drop to send file"),
2400
- /* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
2437
+ /* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { onClick: () => setPubProfile(true), title: T && T.pubOpen || "View public profile", style: { cursor: "pointer", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { onClick: () => setPubProfile(true), style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)", cursor: "pointer" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
2401
2438
  Mono,
2402
2439
  {
2403
2440
  size: 11.5,
@@ -2555,7 +2592,8 @@ ${peer.address}`
2555
2592
  onClick: (e) => e.stopPropagation(),
2556
2593
  style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain", borderRadius: 6 }
2557
2594
  }
2558
- )))
2595
+ ))),
2596
+ pubProfile && /* @__PURE__ */ React.createElement(DkPublicProfile, { T, userid: peer.userId, fallbackName: peer.alias, onClose: () => setPubProfile(false) })
2559
2597
  );
2560
2598
  }
2561
2599
  function MenuItem({ icon, label, onClick, danger }) {
@@ -2890,6 +2928,40 @@ function DkPunkPicker({ T, onPick, onClose }) {
2890
2928
  /* @__PURE__ */ React.createElement("img", { src: p.image, alt: `punk #${p.id}`, style: { width: "100%", height: "100%", imageRendering: "pixelated", display: "block" } })
2891
2929
  ))))));
2892
2930
  }
2931
+ function DkPublicProfile({ T, userid, fallbackName, onClose }) {
2932
+ const [st, setSt] = React.useState({ loading: true });
2933
+ React.useEffect(() => {
2934
+ let dead = false;
2935
+ setSt({ loading: true });
2936
+ fetch(`/api/ens-profile?userid=${encodeURIComponent(userid)}`).then((r) => r.json()).then((d) => {
2937
+ if (!dead)
2938
+ setSt({ loading: false, prof: d.ok && d.profile || null });
2939
+ }).catch(() => {
2940
+ if (!dead)
2941
+ setSt({ loading: false, prof: null });
2942
+ });
2943
+ return () => {
2944
+ dead = true;
2945
+ };
2946
+ }, [userid]);
2947
+ const prof = st.prof;
2948
+ const LINK_BASE = { twitter: "https://x.com/", linkedin: "https://www.linkedin.com/in/", github: "https://github.com/" };
2949
+ const links = prof ? Object.entries(prof.links || {}) : [];
2950
+ const lbl = { width: 110, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 };
2951
+ const rowS = { display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderTop: "1px solid var(--line)" };
2952
+ return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 420, maxWidth: "94vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: "20px 22px", display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14 } }, prof && prof.avatarUrl ? /* @__PURE__ */ React.createElement(DkImgAvatar, { url: prof.avatarUrl, seed: userid, size: 56, radius: 14 }) : prof && prof.punkId != null ? /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: prof.punkId, size: 56, radius: 14, fallbackSeed: userid }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: userid, size: 56, radius: 14 }), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 17, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, prof && prof.displayName || fallbackName || shortKey(userid, 10, 6)), prof && prof.ens && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--accent)", marginTop: 2 } }, prof.ens)), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), st.loading ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : !prof ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.pubNone || "No public profile \u2014 this user has not registered a beagles.eth name.") : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column" } }, prof.description && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--dim)", paddingBottom: 12, lineHeight: 1.5 } }, prof.description), links.length > 0 && /* @__PURE__ */ React.createElement("div", { style: { ...rowS, gap: 8, flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, T.linksCard || "links"), links.map(([k, h]) => /* @__PURE__ */ React.createElement(
2953
+ "a",
2954
+ {
2955
+ key: k,
2956
+ href: (LINK_BASE[k] || "") + h,
2957
+ target: "_blank",
2958
+ rel: "noreferrer",
2959
+ style: { display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 10px", borderRadius: 999, border: "1px solid var(--line)", textDecoration: "none", fontFamily: "var(--ui)", fontSize: 12, color: "var(--accent)" }
2960
+ },
2961
+ k,
2962
+ " \u2197"
2963
+ ))), prof.eth && /* @__PURE__ */ React.createElement("div", { style: rowS }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "ethereum"), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: prof.eth, title: prof.eth }, shortKey(prof.eth, 8, 6))), prof.sol && /* @__PURE__ */ React.createElement("div", { style: rowS }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "solana"), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: prof.sol, title: prof.sol }, shortKey(prof.sol, 8, 6))), /* @__PURE__ */ React.createElement("div", { style: rowS }, /* @__PURE__ */ React.createElement("span", { style: lbl }, T.userId || "user id"), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: userid, title: userid }, shortKey(userid, 10, 6))))));
2964
+ }
2893
2965
  function DkEnsCard({ T, me, onRecord }) {
2894
2966
  const [st, setSt] = React.useState({ loading: true });
2895
2967
  const [label, setLabel] = React.useState("");
@@ -3098,7 +3170,7 @@ function ProfileTab({ T, me, onEdit }) {
3098
3170
  setEditing(false);
3099
3171
  } }));
3100
3172
  }
3101
- Object.assign(window, { ProfileTab, DkPunkAvatar });
3173
+ Object.assign(window, { ProfileTab, DkPunkAvatar, DkPublicProfile });
3102
3174
  function DkDirAvatar({ p, size }) {
3103
3175
  const [broken, setBroken] = React.useState(false);
3104
3176
  if (p.punkId != null && !(p.avatar && !broken)) {
@@ -4142,6 +4214,8 @@ const STR = {
4142
4214
  linksCard: "Links",
4143
4215
  linkPlaceholder: "username or profile URL",
4144
4216
  save: "save",
4217
+ pubOpen: "View public profile",
4218
+ pubNone: "No public profile \u2014 this user has not registered a beagles.eth name.",
4145
4219
  ensNoEthWallet: "no Ethereum wallet extension found (MetaMask\u2026)",
4146
4220
  ensNoSolWallet: "no Solana wallet extension found (Phantom\u2026)",
4147
4221
  ensWalletOwned: "registered via your mobile wallet as",
@@ -4286,6 +4360,8 @@ const STR = {
4286
4360
  linksCard: "\u793E\u4EA4\u8D26\u53F7",
4287
4361
  linkPlaceholder: "\u7528\u6237\u540D\u6216\u4E3B\u9875\u94FE\u63A5",
4288
4362
  save: "\u4FDD\u5B58",
4363
+ pubOpen: "\u67E5\u770B\u516C\u5F00\u8D44\u6599",
4364
+ pubNone: "\u6682\u65E0\u516C\u5F00\u8D44\u6599\u2014\u2014\u5BF9\u65B9\u8FD8\u6CA1\u6709\u6CE8\u518C beagles.eth \u540D\u5B57\u3002",
4289
4365
  ensNoEthWallet: "\u672A\u68C0\u6D4B\u5230\u4EE5\u592A\u574A\u94B1\u5305\u63D2\u4EF6(MetaMask \u7B49)",
4290
4366
  ensNoSolWallet: "\u672A\u68C0\u6D4B\u5230 Solana \u94B1\u5305\u63D2\u4EF6(Phantom \u7B49)",
4291
4367
  ensWalletOwned: "\u5DF2\u901A\u8FC7\u624B\u673A\u94B1\u5305\u6CE8\u518C\u4E3A",
package/dist/server.js CHANGED
@@ -154,6 +154,39 @@ async function ensAvatarDelete(call, fullName) {
154
154
  // best-effort only
155
155
  }
156
156
  }
157
+ function ensPubFromEntry(ens, v) {
158
+ const t = v?.texts ?? {};
159
+ return {
160
+ ens,
161
+ avatarUrl: t.avatar || null,
162
+ punkId: v?.nft === "CryptoPunks" && typeof v?.nftid === "number" && v.nftid > 0 ? v.nftid : null,
163
+ displayName: t.displayName || null,
164
+ description: t.description || null,
165
+ links: {
166
+ ...(t["com.twitter"] ? { twitter: t["com.twitter"] } : {}),
167
+ ...(t["com.linkedin"] ? { linkedin: t["com.linkedin"] } : {}),
168
+ ...(t["com.github"] ? { github: t["com.github"] } : {}),
169
+ },
170
+ eth: v?.addresses?.["60"] || null,
171
+ sol: v?.addresses?.["501"] || null,
172
+ };
173
+ }
174
+ /** userid → EnsPub from the cached /names listing. Bounded wait so the UI
175
+ * poll never blocks on a cold cache — the fetch continues in the background
176
+ * and the next poll finds it cached. */
177
+ async function ensPublicByUserid(maxWaitMs = 300) {
178
+ const p = fetchDiscoverJson(`${ENS_GATEWAY}/names`).catch(() => null);
179
+ const raw = (await Promise.race([p, new Promise((r) => setTimeout(() => r(null), maxWaitMs))]));
180
+ const map = new Map();
181
+ if (raw && typeof raw === "object") {
182
+ for (const [ens, v] of Object.entries(raw)) {
183
+ const uid = v?.texts?.carrierUserId;
184
+ if (uid)
185
+ map.set(uid, ensPubFromEntry(ens, v));
186
+ }
187
+ }
188
+ return map;
189
+ }
157
190
  /** True only when the request came from the local machine. Used to gate the
158
191
  * "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
159
192
  * identity signing to other hosts. The popup always runs in the local user's
@@ -478,9 +511,9 @@ export function startBeagleServer(opts) {
478
511
  }
479
512
  }
480
513
  // Punk directory (proxied): a random batch by type, and one punk by id.
481
- if (req.method === "GET" && url.startsWith("/api/punk-list")) {
514
+ if (req.method === "GET" && url === "/api/punk-list") {
482
515
  try {
483
- const q = new URL(url, "http://localhost");
516
+ const q = new URL(req.url || "", "http://localhost");
484
517
  const type = (q.searchParams.get("type") || "any").toLowerCase().replace(/[^a-z]/g, "") || "any";
485
518
  const limit = Math.min(60, Math.max(1, Number(q.searchParams.get("limit")) || 24));
486
519
  const list = await punksFetch(`/api/punks/filter/${type}/any?limit=${limit}`);
@@ -1092,11 +1125,38 @@ export function startBeagleServer(opts) {
1092
1125
  // One bootstrap call for the desktop UI: composes the design's DK_*
1093
1126
  // shapes (me / peers / requests / exits) from diag + friends-list +
1094
1127
  // ipam + routes, so the client data layer is a single poll.
1128
+ // Public profile card for any userid (friend or directory row): what
1129
+ // they published on their beagles.eth record. Read-only, no signing.
1130
+ if (req.method === "GET" && url === "/api/ens-profile") {
1131
+ const q = new URL(req.url || "", "http://localhost").searchParams;
1132
+ let userid = q.get("userid") || "";
1133
+ if (!userid) {
1134
+ sendJson(res, 400, { ok: false, error: "userid required" });
1135
+ return;
1136
+ }
1137
+ // A 52-char Carrier ADDRESS also works (group cards only have that):
1138
+ // its first 32 bytes are the pubkey, i.e. the userid.
1139
+ try {
1140
+ const bytes = peerAddr.base58ToBytes(userid);
1141
+ if (bytes.length === peerAddr.CARRIER_ADDRESS_SIZE)
1142
+ userid = peerAddr.bytesToBase58(bytes.slice(0, 32));
1143
+ }
1144
+ catch { /* not base58 — leave as-is */ }
1145
+ let prof = (await ensPublicByUserid(1500)).get(userid) ?? null;
1146
+ if (!prof) {
1147
+ const rec = await ensFetchJson(`/getAddress/${encodeURIComponent(userid)}`);
1148
+ if (rec)
1149
+ prof = ensPubFromEntry(rec.name, rec);
1150
+ }
1151
+ sendJson(res, 200, { ok: true, registered: !!prof, profile: prof });
1152
+ return;
1153
+ }
1095
1154
  if (req.method === "GET" && url === "/api/desktop") {
1096
- const [diag, pending, flist] = await Promise.all([
1155
+ const [diag, pending, flist, ensPub] = await Promise.all([
1097
1156
  opts.call({ op: "diag" }),
1098
1157
  opts.call({ op: "friends-pending" }),
1099
1158
  opts.call({ op: "friends-list" }),
1159
+ ensPublicByUserid(),
1100
1160
  ]);
1101
1161
  const d = (diag.ok ? diag.data : {}) ?? {};
1102
1162
  const identity = d.identity ?? {};
@@ -1147,6 +1207,8 @@ export function startBeagleServer(opts) {
1147
1207
  // null = backend didn't report it (daemon); the UI then hides the
1148
1208
  // toggle rather than showing a state it cannot know.
1149
1209
  autoAccept: typeof node.autoAccept === "boolean" ? node.autoAccept : null,
1210
+ avatarUrl: ensPub.get(identity.userid ?? "")?.avatarUrl ?? null,
1211
+ punkId: ensPub.get(identity.userid ?? "")?.punkId ?? null,
1150
1212
  };
1151
1213
  const fl = (flist.ok ? (flist.data?.friends ?? []) : []);
1152
1214
  const peers = fl.map((f) => {
@@ -1177,6 +1239,8 @@ export function startBeagleServer(opts) {
1177
1239
  lastMsg: lm ? (lm.dir === "out" ? "you: " : "") + (lm.text ?? "") : "",
1178
1240
  lastTime: fmtTime(lm?.ts),
1179
1241
  wire: "163",
1242
+ avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
1243
+ punkId: ensPub.get(uid)?.punkId ?? null,
1180
1244
  };
1181
1245
  });
1182
1246
  const pend = pending.ok ? (pending.data?.pending ?? []) : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
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",