@decentnetwork/beagle 0.1.18 → 0.1.19

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.17";
1
+ window.__DK_UI_VERSION="0.1.19";
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"/>',
@@ -2800,10 +2800,93 @@ function DkEditModal({ T, me, onClose, onSave }) {
2800
2800
  };
2801
2801
  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: 440, maxWidth: "92vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 22, display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, T.editProfile), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "display name"), /* @__PURE__ */ React.createElement("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: onKey, autoFocus: true, maxLength: 48, style: field })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "status message"), /* @__PURE__ */ React.createElement("input", { value: desc, onChange: (e) => setDesc(e.target.value), onKeyDown: onKey, maxLength: 120, placeholder: "optional \u2014 a short bio friends will see", style: field })), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, "Your userid (the unique identity) can't change \u2014 only the display name + status."), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: onClose }, T.cancel || "cancel"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", onClick: save }, T.save || "save"))));
2802
2802
  }
2803
+ function DkEnsCard({ T, me }) {
2804
+ const [st, setSt] = React.useState({ loading: true });
2805
+ const [label, setLabel] = React.useState("");
2806
+ const [busy, setBusy] = React.useState(false);
2807
+ const [msg, setMsg] = React.useState(null);
2808
+ const reload = () => {
2809
+ fetch("/api/ens-name").then((r) => r.json()).then((d) => {
2810
+ setSt({ loading: false, registered: !!d.registered, record: d.record || null, mineOwned: !!d.mineOwned });
2811
+ if (d.registered && d.record && d.record.name)
2812
+ setLabel(d.record.name.replace(/\.beagles\.eth$/i, ""));
2813
+ }).catch((e) => setSt({ loading: false, error: String(e && e.message || e) }));
2814
+ };
2815
+ React.useEffect(() => {
2816
+ reload();
2817
+ }, []);
2818
+ const post = (url, body) => {
2819
+ setBusy(true);
2820
+ setMsg(null);
2821
+ return fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }).then((r) => r.json()).then((d) => {
2822
+ setMsg(d.ok ? { tone: "ok", text: T.ensDone || "saved" } : { tone: "err", text: d.error || "failed" });
2823
+ if (d.ok)
2824
+ reload();
2825
+ return d;
2826
+ }).catch((e) => {
2827
+ setMsg({ tone: "err", text: String(e && e.message || e) });
2828
+ }).finally(() => setBusy(false));
2829
+ };
2830
+ const register = () => {
2831
+ const l = label.trim();
2832
+ if (l)
2833
+ post("/api/ens-register", { name: l, displayName: me.name });
2834
+ };
2835
+ const bind = (chain, address) => post("/api/ens-bind-wallet", { chain, address });
2836
+ const bindEth = async () => {
2837
+ const eth = window.ethereum;
2838
+ if (!eth) {
2839
+ setMsg({ tone: "err", text: T.ensNoEthWallet || "no Ethereum wallet extension found (MetaMask\u2026)" });
2840
+ return;
2841
+ }
2842
+ try {
2843
+ const a = await eth.request({ method: "eth_requestAccounts" });
2844
+ if (a && a[0])
2845
+ bind("eth", a[0]);
2846
+ } catch (e) {
2847
+ setMsg({ tone: "err", text: String(e && e.message || e) });
2848
+ }
2849
+ };
2850
+ const bindSol = async () => {
2851
+ const sol = window.phantom && window.phantom.solana || window.solana;
2852
+ if (!sol) {
2853
+ setMsg({ tone: "err", text: T.ensNoSolWallet || "no Solana wallet extension found (Phantom\u2026)" });
2854
+ return;
2855
+ }
2856
+ try {
2857
+ const r = await sol.connect();
2858
+ const a = r && r.publicKey && r.publicKey.toString();
2859
+ if (a)
2860
+ bind("sol", a);
2861
+ } catch (e) {
2862
+ setMsg({ tone: "err", text: String(e && e.message || e) });
2863
+ }
2864
+ };
2865
+ 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" };
2866
+ const row = { display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", borderBottom: "1px solid var(--line)" };
2867
+ const rec = st.record;
2868
+ const boundEth = rec && rec.addresses && rec.addresses["60"];
2869
+ const boundSol = rec && rec.addresses && rec.addresses["501"];
2870
+ const walletRow = (lbl, 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("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 }, bound ? T.ensRebind || "rebind" : T.ensBind || "bind"));
2871
+ 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(
2872
+ "input",
2873
+ {
2874
+ value: label,
2875
+ onChange: (e) => setLabel(e.target.value),
2876
+ placeholder: T.ensPlaceholder || "yourname",
2877
+ disabled: busy,
2878
+ onKeyDown: (e) => {
2879
+ if (e.key === "Enter")
2880
+ register();
2881
+ },
2882
+ style: field
2883
+ }
2884
+ ), /* @__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)), walletRow(T.ensEth || "ethereum", boundEth, bindEth, false), walletRow(T.ensSol || "solana", 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)));
2885
+ }
2803
2886
  function ProfileTab({ T, me, onEdit }) {
2804
2887
  const [qr, setQr] = React.useState(null);
2805
2888
  const [editing, setEditing] = React.useState(false);
2806
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 18, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.netKey, value: me.netKey, copy: true, last: true })), typeof me.autoAccept === "boolean" && /* @__PURE__ */ React.createElement(Card, { label: T && T.friendsSettings || "Friends" }, /* @__PURE__ */ React.createElement("label", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px", cursor: "pointer" } }, /* @__PURE__ */ React.createElement(
2889
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 18, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(DkEnsCard, { T, me }), typeof me.autoAccept === "boolean" && /* @__PURE__ */ React.createElement(Card, { label: T && T.friendsSettings || "Friends" }, /* @__PURE__ */ React.createElement("label", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px", cursor: "pointer" } }, /* @__PURE__ */ React.createElement(
2807
2890
  "input",
2808
2891
  {
2809
2892
  type: "checkbox",
@@ -3761,7 +3844,7 @@ const STR = {
3761
3844
  dirLoading: "loading directory\u2026",
3762
3845
  dirEmpty: "nobody matches",
3763
3846
  dirError: "Could not load the directory:",
3764
- addPlaceholder: "paste a friend's carrier address\u2026",
3847
+ addPlaceholder: "carrier address or name.beagles.eth\u2026",
3765
3848
  add: "Add",
3766
3849
  search: "search peers / ip\u2026",
3767
3850
  addSending: "sending friend-request\u2026",
@@ -3832,6 +3915,20 @@ const STR = {
3832
3915
  userId: "user id",
3833
3916
  carrierAddr: "carrier address",
3834
3917
  netKey: "network key",
3918
+ ensCard: "Name \xB7 beagles.eth",
3919
+ ensName: "name",
3920
+ ensPlaceholder: "yourname",
3921
+ ensRegister: "register",
3922
+ ensUpdate: "update",
3923
+ ensDone: "saved",
3924
+ ensEth: "ethereum",
3925
+ ensSol: "solana",
3926
+ ensBind: "bind",
3927
+ ensRebind: "rebind",
3928
+ ensNotBound: "not bound",
3929
+ ensNoEthWallet: "no Ethereum wallet extension found (MetaMask\u2026)",
3930
+ ensNoSolWallet: "no Solana wallet extension found (Phantom\u2026)",
3931
+ ensWalletOwned: "registered via your mobile wallet as",
3835
3932
  virtualIp: "virtual ip",
3836
3933
  version: "version",
3837
3934
  editProfile: "edit",
@@ -3872,7 +3969,7 @@ const STR = {
3872
3969
  dirLoading: "\u6B63\u5728\u52A0\u8F7D\u540D\u5355\u2026",
3873
3970
  dirEmpty: "\u6CA1\u6709\u5339\u914D\u7684\u4EBA",
3874
3971
  dirError: "\u540D\u5355\u52A0\u8F7D\u5931\u8D25\uFF1A",
3875
- addPlaceholder: "\u7C98\u8D34\u597D\u53CB\u7684 carrier \u5730\u5740\u2026",
3972
+ addPlaceholder: "carrier \u5730\u5740\u6216 \u540D\u5B57.beagles.eth\u2026",
3876
3973
  add: "\u6DFB\u52A0",
3877
3974
  search: "\u641C\u7D22\u597D\u53CB / IP\u2026",
3878
3975
  addSending: "\u6B63\u5728\u53D1\u9001\u597D\u53CB\u8BF7\u6C42\u2026",
@@ -3943,6 +4040,20 @@ const STR = {
3943
4040
  userId: "\u7528\u6237 ID",
3944
4041
  carrierAddr: "Carrier \u5730\u5740",
3945
4042
  netKey: "\u7F51\u7EDC\u516C\u94A5",
4043
+ ensCard: "\u540D\u5B57 \xB7 beagles.eth",
4044
+ ensName: "\u540D\u5B57",
4045
+ ensPlaceholder: "\u4F60\u7684\u540D\u5B57",
4046
+ ensRegister: "\u6CE8\u518C",
4047
+ ensUpdate: "\u66F4\u65B0",
4048
+ ensDone: "\u5DF2\u4FDD\u5B58",
4049
+ ensEth: "ethereum",
4050
+ ensSol: "solana",
4051
+ ensBind: "\u7ED1\u5B9A",
4052
+ ensRebind: "\u91CD\u65B0\u7ED1\u5B9A",
4053
+ ensNotBound: "\u672A\u7ED1\u5B9A",
4054
+ ensNoEthWallet: "\u672A\u68C0\u6D4B\u5230\u4EE5\u592A\u574A\u94B1\u5305\u63D2\u4EF6(MetaMask \u7B49)",
4055
+ ensNoSolWallet: "\u672A\u68C0\u6D4B\u5230 Solana \u94B1\u5305\u63D2\u4EF6(Phantom \u7B49)",
4056
+ ensWalletOwned: "\u5DF2\u901A\u8FC7\u624B\u673A\u94B1\u5305\u6CE8\u518C\u4E3A",
3946
4057
  virtualIp: "\u865A\u62DF IP",
3947
4058
  version: "\u7248\u672C",
3948
4059
  editProfile: "\u7F16\u8F91",
package/dist/server.js CHANGED
@@ -51,6 +51,66 @@ async function fetchDiscoverJson(url) {
51
51
  clearTimeout(timer);
52
52
  }
53
53
  }
54
+ // ── beagles.eth names (ens-gateway worker) ──────────────────────────────────
55
+ // The same registry the mobile apps register into. Web-side registration
56
+ // signs the record with THIS node's Carrier identity (XEdDSA via the backend
57
+ // `sign` op) — the gateway verifies it the same way Sign in with Decent does.
58
+ const ENS_GATEWAY = process.env.BEAGLE_ENS_GATEWAY || "https://ens-gateway.beaglechat.workers.dev";
59
+ async function ensFetchJson(path) {
60
+ const ctl = new AbortController();
61
+ const timer = setTimeout(() => ctl.abort(), 10_000);
62
+ try {
63
+ const r = await fetch(`${ENS_GATEWAY}${path}`, { signal: ctl.signal });
64
+ if (!r.ok)
65
+ return null;
66
+ const d = (await r.json());
67
+ return d && typeof d === "object" && d.name ? d : null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ finally {
73
+ clearTimeout(timer);
74
+ }
75
+ }
76
+ /** Sign a name record with the Carrier identity and POST it to the gateway.
77
+ * The record itself (as a JSON string) is what gets signed; the gateway
78
+ * re-parses `signature.message` and verifies against `owner` inside it. */
79
+ async function ensSignAndSet(call, record) {
80
+ const message = JSON.stringify(record);
81
+ const signed = await call({ op: "sign", text: message });
82
+ if (!signed.ok)
83
+ return { ok: false, error: signed.error || "identity sign failed" };
84
+ const sig = signed.data?.sig ?? "";
85
+ if (!sig)
86
+ return { ok: false, error: "identity sign returned no signature" };
87
+ const payload = {
88
+ ...record,
89
+ expiration: Math.floor(Date.now() / 1000) + 3600,
90
+ signature: { hash: sig, signerAddress: record.owner, message },
91
+ };
92
+ const ctl = new AbortController();
93
+ const timer = setTimeout(() => ctl.abort(), 15_000);
94
+ try {
95
+ const r = await fetch(`${ENS_GATEWAY}/set`, {
96
+ method: "POST",
97
+ headers: { "content-type": "application/json" },
98
+ body: JSON.stringify(payload),
99
+ signal: ctl.signal,
100
+ });
101
+ const d = (await r.json().catch(() => null));
102
+ if (!r.ok || !d?.success) {
103
+ return { ok: false, error: typeof d?.error === "string" ? d.error : `gateway HTTP ${r.status}` };
104
+ }
105
+ return { ok: true };
106
+ }
107
+ catch (e) {
108
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
109
+ }
110
+ finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
54
114
  /** True only when the request came from the local machine. Used to gate the
55
115
  * "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
56
116
  * identity signing to other hosts. The popup always runs in the local user's
@@ -364,6 +424,116 @@ export function startBeagleServer(opts) {
364
424
  });
365
425
  return;
366
426
  }
427
+ // ── beagles.eth name registration ──
428
+ // Register/update this node's *.beagles.eth name and bind wallet
429
+ // addresses into the record. The write routes trigger identity
430
+ // signatures, so they're LOCALHOST-ONLY like /connect.
431
+ if (url === "/api/ens-register" || url === "/api/ens-bind-wallet") {
432
+ if (!isLocalRequest(req)) {
433
+ sendJson(res, 403, { ok: false, error: "identity signing is available only on this machine (localhost)" });
434
+ return;
435
+ }
436
+ }
437
+ if (req.method === "GET" && url === "/api/ens-name") {
438
+ const diag = await opts.call({ op: "diag" });
439
+ const identity = (diag.ok ? diag.data?.identity : null) ?? {};
440
+ if (!identity.userid) {
441
+ sendJson(res, 502, { ok: false, error: "identity unavailable" });
442
+ return;
443
+ }
444
+ const rec = await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`);
445
+ sendJson(res, 200, { ok: true, registered: !!rec, record: rec, mineOwned: !!rec && rec.owner === identity.userid });
446
+ return;
447
+ }
448
+ if (req.method === "POST" && url === "/api/ens-register") {
449
+ const body = await readBody(req);
450
+ const rawName = typeof body.name === "string" ? body.name.trim().toLowerCase() : "";
451
+ const label = rawName.replace(/\.beagles\.eth$/, "");
452
+ if (!/^[a-z0-9-]{1,63}$/.test(label)) {
453
+ sendJson(res, 400, { ok: false, error: "name must be 1-63 chars of a-z, 0-9, hyphen" });
454
+ return;
455
+ }
456
+ const fullName = `${label}.beagles.eth`;
457
+ const diag = await opts.call({ op: "diag" });
458
+ const identity = (diag.ok ? diag.data?.identity : null) ?? {};
459
+ if (!identity.userid || !identity.address) {
460
+ sendJson(res, 502, { ok: false, error: "identity unavailable" });
461
+ return;
462
+ }
463
+ const existing = await ensFetchJson(`/get/${encodeURIComponent(fullName)}`);
464
+ if (existing && existing.owner !== identity.userid) {
465
+ sendJson(res, 409, { ok: false, error: `'${fullName}' is already taken` });
466
+ return;
467
+ }
468
+ // Carry wallet binds forward: from the same record on update, or from
469
+ // my previous record when re-registering under a new label.
470
+ const mine = existing ?? (await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`));
471
+ const carried = mine && mine.owner === identity.userid ? mine : null;
472
+ const displayName = (typeof body.displayName === "string" && body.displayName.trim().slice(0, 64)) || label;
473
+ const description = (typeof body.description === "string" && body.description.trim().slice(0, 200)) ||
474
+ carried?.texts?.description ||
475
+ `${displayName}'s Beagle Chat Profile`;
476
+ const r = await ensSignAndSet(opts.call, {
477
+ name: fullName,
478
+ owner: identity.userid,
479
+ addresses: carried?.addresses ?? {},
480
+ texts: {
481
+ ...(carried?.texts ?? {}),
482
+ description,
483
+ displayName,
484
+ carrierAddress: identity.address,
485
+ carrierUserId: identity.userid,
486
+ },
487
+ referee: carried?.referee ?? "",
488
+ nft: carried?.nft ?? "",
489
+ nftid: carried?.nftid ?? 0,
490
+ });
491
+ sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, name: fullName } : { ok: false, error: r.error });
492
+ return;
493
+ }
494
+ if (req.method === "POST" && url === "/api/ens-bind-wallet") {
495
+ const body = await readBody(req);
496
+ const coinType = body.chain === "eth" ? "60" : body.chain === "sol" ? "501" : null;
497
+ const addr = typeof body.address === "string" ? body.address.trim() : "";
498
+ const remove = addr === "";
499
+ const okAddr = remove
500
+ || (coinType === "60" ? /^0x[0-9a-fA-F]{40}$/.test(addr) : /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(addr));
501
+ if (!coinType || !okAddr) {
502
+ sendJson(res, 400, { ok: false, error: "invalid chain or address" });
503
+ return;
504
+ }
505
+ const diag = await opts.call({ op: "diag" });
506
+ const identity = (diag.ok ? diag.data?.identity : null) ?? {};
507
+ if (!identity.userid) {
508
+ sendJson(res, 502, { ok: false, error: "identity unavailable" });
509
+ return;
510
+ }
511
+ const mine = await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`);
512
+ if (!mine) {
513
+ sendJson(res, 404, { ok: false, error: "register a name first" });
514
+ return;
515
+ }
516
+ if (mine.owner !== identity.userid) {
517
+ sendJson(res, 403, { ok: false, error: "this name was registered by your mobile wallet — bind addresses there" });
518
+ return;
519
+ }
520
+ const addresses = { ...(mine.addresses ?? {}) };
521
+ if (remove)
522
+ delete addresses[coinType];
523
+ else
524
+ addresses[coinType] = addr;
525
+ const r = await ensSignAndSet(opts.call, {
526
+ name: mine.name,
527
+ owner: identity.userid,
528
+ addresses,
529
+ texts: mine.texts ?? {},
530
+ referee: mine.referee ?? "",
531
+ nft: mine.nft ?? "",
532
+ nftid: mine.nftid ?? 0,
533
+ });
534
+ sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, addresses } : { ok: false, error: r.error });
535
+ return;
536
+ }
367
537
  // Delete file/chat messages by id (removes their on-disk files too).
368
538
  if (req.method === "POST" && url === "/api/file-delete") {
369
539
  const body = await readBody(req);
@@ -1056,6 +1226,31 @@ export function startBeagleServer(opts) {
1056
1226
  }
1057
1227
  if (req.method === "POST" && url === "/api/add") {
1058
1228
  let { address } = await readBody(req);
1229
+ // Accept a *.beagles.eth name (or bare label): resolve it to the
1230
+ // Carrier address via the ens-gateway registry the mobile apps
1231
+ // register into. Anything with a dot/space or too short to be a
1232
+ // userid (44) / address (52) is treated as a name.
1233
+ {
1234
+ const raw = String(address ?? "").trim();
1235
+ if (raw && (raw.includes(".") || raw.includes(" ") || raw.length < 40)) {
1236
+ const full = raw.includes(".") ? raw : `${raw}.beagles.eth`;
1237
+ const norm = (s) => s.toLowerCase().replace(/\s+/g, "");
1238
+ try {
1239
+ const names = (await fetchDiscoverJson("https://ens-gateway.beaglechat.workers.dev/names"));
1240
+ const hit = Object.entries(names ?? {}).find(([k]) => norm(k) === norm(full));
1241
+ const carrier = hit?.[1]?.texts?.carrierAddress;
1242
+ if (!carrier) {
1243
+ sendJson(res, 400, { ok: false, error: `name '${full}' is not registered on beagles.eth` });
1244
+ return;
1245
+ }
1246
+ address = carrier;
1247
+ }
1248
+ catch (e) {
1249
+ sendJson(res, 502, { ok: false, error: `name lookup failed: ${e instanceof Error ? e.message : String(e)}` });
1250
+ return;
1251
+ }
1252
+ }
1253
+ }
1059
1254
  // Accept a 44-char userid too: the address is userid's pubkey plus a
1060
1255
  // derivable suffix (nospam=0 network-wide + checksum), and users
1061
1256
  // frequently have only the userid — it's what every other surface
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
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",