@decentnetwork/beagle 0.1.22 → 0.1.23

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.21";
1
+ window.__DK_UI_VERSION="0.1.22";
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"/>',
@@ -2956,6 +2956,47 @@ function DkEnsCard({ T, me, onRecord }) {
2956
2956
  };
2957
2957
  const field = { flex: 1, minWidth: 0, height: 34, borderRadius: 8, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--mono)", fontSize: 13, padding: "0 10px", outline: "none" };
2958
2958
  const row = { display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", borderBottom: "1px solid var(--line)" };
2959
+ const rowLbl = { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 };
2960
+ const SOCIALS = [
2961
+ { k: "twitter", base: "https://x.com/" },
2962
+ { k: "linkedin", base: "https://www.linkedin.com/in/" },
2963
+ { k: "github", base: "https://github.com/" }
2964
+ ];
2965
+ const [draft, setDraft] = React.useState({});
2966
+ React.useEffect(() => {
2967
+ const t = st.record && st.record.texts || {};
2968
+ setDraft({ twitter: t["com.twitter"] || "", linkedin: t["com.linkedin"] || "", github: t["com.github"] || "" });
2969
+ }, [st.record]);
2970
+ const saveSocial = (k) => post("/api/ens-social", { key: k, value: (draft[k] || "").trim() });
2971
+ const socialRow = ({ k, base }, last) => {
2972
+ var _a;
2973
+ const saved = (st.record && st.record.texts || {})["com." + k] || "";
2974
+ const cur = (_a = draft[k]) != null ? _a : "";
2975
+ return /* @__PURE__ */ React.createElement("div", { key: k, style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { style: rowLbl }, k), /* @__PURE__ */ React.createElement(
2976
+ "input",
2977
+ {
2978
+ value: cur,
2979
+ onChange: (e) => setDraft({ ...draft, [k]: e.target.value }),
2980
+ disabled: busy || !st.mineOwned,
2981
+ placeholder: T.linkPlaceholder || "username or profile URL",
2982
+ onKeyDown: (e) => {
2983
+ if (e.key === "Enter")
2984
+ saveSocial(k);
2985
+ },
2986
+ style: field
2987
+ }
2988
+ ), saved && /* @__PURE__ */ React.createElement(
2989
+ "a",
2990
+ {
2991
+ href: base + saved,
2992
+ target: "_blank",
2993
+ rel: "noreferrer",
2994
+ title: base + saved,
2995
+ style: { flexShrink: 0, fontFamily: "var(--mono)", fontSize: 13, color: "var(--accent)", textDecoration: "none" }
2996
+ },
2997
+ "\u2197"
2998
+ ), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !st.mineOwned || cur.trim() === saved, onClick: () => saveSocial(k) }, saved && !cur.trim() ? T.ensUnbind || "unbind" : T.save || "save"));
2999
+ };
2959
3000
  const [picking, setPicking] = React.useState(false);
2960
3001
  const fileRef = React.useRef(null);
2961
3002
  const rec = st.record;
@@ -3018,7 +3059,7 @@ function DkEnsCard({ T, me, onRecord }) {
3018
3059
  img.src = url;
3019
3060
  };
3020
3061
  const walletRow = (lbl, chain, bound, onBind, last) => /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, lbl), bound ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, bound), /* @__PURE__ */ React.createElement(CopyBtn, { value: bound, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => bind(chain, "") }, T.ensUnbind || "unbind")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotBound || "not bound"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: onBind }, T.ensBind || "bind")));
3021
- return /* @__PURE__ */ React.createElement(Card, { label: T.ensCard || "Name \xB7 beagles.eth" }, st.loading ? /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensName || "name"), /* @__PURE__ */ React.createElement(
3062
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Card, { label: T.ensCard || "Name \xB7 beagles.eth" }, st.loading ? /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensName || "name"), /* @__PURE__ */ React.createElement(
3022
3063
  "input",
3023
3064
  {
3024
3065
  value: label,
@@ -3031,7 +3072,7 @@ function DkEnsCard({ T, me, onRecord }) {
3031
3072
  },
3032
3073
  style: field
3033
3074
  }
3034
- ), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--dim)", flexShrink: 0 } }, ".beagles.eth"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !label.trim(), onClick: register }, busy ? "\u2026" : st.registered && st.mineOwned ? T.ensUpdate || "update" : T.ensRegister || "register")), st.registered && !st.mineOwned && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", borderBottom: "1px solid var(--line)", fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, (T.ensWalletOwned || "registered via your mobile wallet as") + " ", /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)" } }, rec && rec.name)), st.registered && /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensAvatar || "avatar"), upAvatar ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("img", { src: upAvatar, alt: "", width: 34, height: 34, style: { width: 34, height: 34, borderRadius: 9, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" } }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--text)" } }, T.ensCustom || "custom image"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : punkId != null ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 34, radius: 9, fallbackSeed: me.userId }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, `CryptoPunk #${punkId}`), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotSet || "not set"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => fileRef.current && fileRef.current.click() }, T.ensUpload || "upload"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => setPicking(true) }, T.ensChoose || "choose"), /* @__PURE__ */ React.createElement("input", { ref: fileRef, type: "file", accept: "image/*", onChange: onUploadFile, style: { display: "none" } })), walletRow(T.ensEth || "ethereum", "eth", boundEth, bindEth, false), walletRow(T.ensSol || "solana", "sol", boundSol, bindSol, !msg), msg && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", fontFamily: "var(--ui)", fontSize: 12, color: msg.tone === "ok" ? "var(--online)" : "var(--danger)" } }, msg.text)), picking && /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) }));
3075
+ ), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--dim)", flexShrink: 0 } }, ".beagles.eth"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !label.trim(), onClick: register }, busy ? "\u2026" : st.registered && st.mineOwned ? T.ensUpdate || "update" : T.ensRegister || "register")), st.registered && !st.mineOwned && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", borderBottom: "1px solid var(--line)", fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, (T.ensWalletOwned || "registered via your mobile wallet as") + " ", /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)" } }, rec && rec.name)), st.registered && /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensAvatar || "avatar"), upAvatar ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("img", { src: upAvatar, alt: "", width: 34, height: 34, style: { width: 34, height: 34, borderRadius: 9, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" } }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--text)" } }, T.ensCustom || "custom image"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : punkId != null ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 34, radius: 9, fallbackSeed: me.userId }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, `CryptoPunk #${punkId}`), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotSet || "not set"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => fileRef.current && fileRef.current.click() }, T.ensUpload || "upload"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => setPicking(true) }, T.ensChoose || "choose"), /* @__PURE__ */ React.createElement("input", { ref: fileRef, type: "file", accept: "image/*", onChange: onUploadFile, style: { display: "none" } })), walletRow(T.ensEth || "ethereum", "eth", boundEth, bindEth, false), walletRow(T.ensSol || "solana", "sol", boundSol, bindSol, !msg), msg && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", fontFamily: "var(--ui)", fontSize: 12, color: msg.tone === "ok" ? "var(--online)" : "var(--danger)" } }, msg.text)), picking && /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) })), !st.loading && st.registered && /* @__PURE__ */ React.createElement(Card, { label: T.linksCard || "Links" }, SOCIALS.map((s, i) => socialRow(s, i === SOCIALS.length - 1))));
3035
3076
  }
3036
3077
  function ProfileTab({ T, me, onEdit }) {
3037
3078
  const [qr, setQr] = React.useState(null);
@@ -4098,6 +4139,9 @@ const STR = {
4098
4139
  pt_zombie: "zombie",
4099
4140
  pt_ape: "ape",
4100
4141
  pt_alien: "alien",
4142
+ linksCard: "Links",
4143
+ linkPlaceholder: "username or profile URL",
4144
+ save: "save",
4101
4145
  ensNoEthWallet: "no Ethereum wallet extension found (MetaMask\u2026)",
4102
4146
  ensNoSolWallet: "no Solana wallet extension found (Phantom\u2026)",
4103
4147
  ensWalletOwned: "registered via your mobile wallet as",
@@ -4239,6 +4283,9 @@ const STR = {
4239
4283
  pt_zombie: "\u50F5\u5C38",
4240
4284
  pt_ape: "\u733F",
4241
4285
  pt_alien: "\u5916\u661F\u4EBA",
4286
+ linksCard: "\u793E\u4EA4\u8D26\u53F7",
4287
+ linkPlaceholder: "\u7528\u6237\u540D\u6216\u4E3B\u9875\u94FE\u63A5",
4288
+ save: "\u4FDD\u5B58",
4242
4289
  ensNoEthWallet: "\u672A\u68C0\u6D4B\u5230\u4EE5\u592A\u574A\u94B1\u5305\u63D2\u4EF6(MetaMask \u7B49)",
4243
4290
  ensNoSolWallet: "\u672A\u68C0\u6D4B\u5230 Solana \u94B1\u5305\u63D2\u4EF6(Phantom \u7B49)",
4244
4291
  ensWalletOwned: "\u5DF2\u901A\u8FC7\u624B\u673A\u94B1\u5305\u6CE8\u518C\u4E3A",
package/dist/server.js CHANGED
@@ -471,7 +471,7 @@ export function startBeagleServer(opts) {
471
471
  // Register/update this node's *.beagles.eth name and bind wallet
472
472
  // addresses into the record. The write routes trigger identity
473
473
  // signatures, so they're LOCALHOST-ONLY like /connect.
474
- if (url === "/api/ens-register" || url === "/api/ens-bind-wallet" || url === "/api/ens-avatar" || url === "/api/ens-avatar-upload") {
474
+ if (url === "/api/ens-register" || url === "/api/ens-bind-wallet" || url === "/api/ens-avatar" || url === "/api/ens-avatar-upload" || url === "/api/ens-social") {
475
475
  if (!isLocalRequest(req)) {
476
476
  sendJson(res, 403, { ok: false, error: "identity signing is available only on this machine (localhost)" });
477
477
  return;
@@ -549,6 +549,59 @@ export function startBeagleServer(opts) {
549
549
  sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, nftid: clear ? null : nftid } : { ok: false, error: r.error });
550
550
  return;
551
551
  }
552
+ // Bind/update/remove a social handle on the record, stored under the
553
+ // standard ENS text keys (com.twitter / com.linkedin / com.github) so
554
+ // other ENS-aware clients can read them too. Empty value = remove.
555
+ if (req.method === "POST" && url === "/api/ens-social") {
556
+ const SOCIAL_KEYS = { twitter: "com.twitter", linkedin: "com.linkedin", github: "com.github" };
557
+ const body = await readBody(req);
558
+ const key = SOCIAL_KEYS[String(body.key ?? "")];
559
+ if (!key) {
560
+ sendJson(res, 400, { ok: false, error: "key must be twitter, linkedin or github" });
561
+ return;
562
+ }
563
+ // Accept a bare handle, @handle, or a pasted profile URL — keep the handle.
564
+ const value = String(body.value ?? "")
565
+ .trim()
566
+ .replace(/^https?:\/\/(www\.)?(x\.com|twitter\.com|github\.com|linkedin\.com\/(in|company))\//i, "")
567
+ .replace(/\/+$/, "")
568
+ .replace(/^@/, "");
569
+ if (value && !/^[A-Za-z0-9_.-]{1,64}$/.test(value)) {
570
+ sendJson(res, 400, { ok: false, error: "handle may only contain letters, digits, _ . -" });
571
+ return;
572
+ }
573
+ const diag = await opts.call({ op: "diag" });
574
+ const identity = (diag.ok ? diag.data?.identity : null) ?? {};
575
+ if (!identity.userid) {
576
+ sendJson(res, 502, { ok: false, error: "identity unavailable" });
577
+ return;
578
+ }
579
+ const mine = await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`);
580
+ if (!mine) {
581
+ sendJson(res, 404, { ok: false, error: "register a name first" });
582
+ return;
583
+ }
584
+ if (mine.owner !== identity.userid) {
585
+ sendJson(res, 403, { ok: false, error: "this name was registered by your mobile wallet — edit links there" });
586
+ return;
587
+ }
588
+ const texts = { ...(mine.texts ?? {}) };
589
+ if (value)
590
+ texts[key] = value;
591
+ else
592
+ delete texts[key];
593
+ const r = await ensSignAndSet(opts.call, {
594
+ name: mine.name,
595
+ owner: identity.userid,
596
+ addresses: mine.addresses ?? {},
597
+ texts,
598
+ referee: mine.referee ?? "",
599
+ nft: mine.nft ?? "",
600
+ nftid: mine.nftid ?? 0,
601
+ });
602
+ sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, key: body.key, value: value || null } : { ok: false, error: r.error });
603
+ return;
604
+ }
552
605
  // Upload a custom avatar image: the browser sends a pre-scaled data URL,
553
606
  // we push the bytes to the gateway's R2 (auth = identity signature) and
554
607
  // point texts.avatar at it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
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",