@decentnetwork/beagle 0.1.17 → 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.
- package/dist/desktop/app.js +244 -5
- package/dist/server.js +261 -0
- package/package.json +1 -1
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
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",
|
|
@@ -2822,6 +2905,112 @@ function ProfileTab({ T, me, onEdit }) {
|
|
|
2822
2905
|
} }));
|
|
2823
2906
|
}
|
|
2824
2907
|
Object.assign(window, { ProfileTab });
|
|
2908
|
+
function DkDirAvatar({ p, size }) {
|
|
2909
|
+
const [broken, setBroken] = React.useState(false);
|
|
2910
|
+
if (p.avatar && !broken) {
|
|
2911
|
+
return /* @__PURE__ */ React.createElement(
|
|
2912
|
+
"img",
|
|
2913
|
+
{
|
|
2914
|
+
src: p.avatar,
|
|
2915
|
+
alt: "",
|
|
2916
|
+
width: size,
|
|
2917
|
+
height: size,
|
|
2918
|
+
onError: () => setBroken(true),
|
|
2919
|
+
style: { width: size, height: size, borderRadius: Math.round(size / 4), objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" }
|
|
2920
|
+
}
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
return /* @__PURE__ */ React.createElement(DkIdenticon, { seed: p.userid || p.name, size, radius: Math.round(size / 4) });
|
|
2924
|
+
}
|
|
2925
|
+
function DkDirRow({ p, T, isFriend, isMe, onAdd, onOpenChat }) {
|
|
2926
|
+
const [state, setState] = React.useState(null);
|
|
2927
|
+
const add = () => {
|
|
2928
|
+
setState("busy");
|
|
2929
|
+
Promise.resolve(onAdd(p.address)).then((r) => setState(r && r.ok === false ? r.error || T.addFailed || "failed" : "sent")).catch((e) => setState(String(e && e.message || e)));
|
|
2930
|
+
};
|
|
2931
|
+
const btn = (label, onClick, disabled) => /* @__PURE__ */ React.createElement(
|
|
2932
|
+
"button",
|
|
2933
|
+
{
|
|
2934
|
+
onClick,
|
|
2935
|
+
disabled,
|
|
2936
|
+
style: {
|
|
2937
|
+
padding: "5px 11px",
|
|
2938
|
+
borderRadius: 8,
|
|
2939
|
+
border: "1px solid var(--line)",
|
|
2940
|
+
flexShrink: 0,
|
|
2941
|
+
background: disabled ? "transparent" : "var(--accent)",
|
|
2942
|
+
color: disabled ? "var(--faint)" : "#fff",
|
|
2943
|
+
fontFamily: "var(--ui)",
|
|
2944
|
+
fontSize: 12,
|
|
2945
|
+
cursor: disabled ? "default" : "pointer"
|
|
2946
|
+
}
|
|
2947
|
+
},
|
|
2948
|
+
label
|
|
2949
|
+
);
|
|
2950
|
+
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 11, padding: "var(--row-pad)", borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement(DkDirAvatar, { p, size: 34 }), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "baseline", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 13.5, fontWeight: 600, color: "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.name), p.points != null && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--accent)", flexShrink: 0 } }, p.points.toLocaleString(), " pts"), p.ens && p.ens !== p.name && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.ens)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement(Mono, { size: 10.5, dim: true, copy: p.userid, title: p.userid }, shortKey(p.userid, 10, 6)), p.description && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.description))), isMe ? /* @__PURE__ */ React.createElement(Tag, null, T.dirMe || "me") : isFriend ? btn(T.dirOpenChat || "chat", () => onOpenChat(p.userid), false) : state === "sent" ? /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, T.dirSent || "requested") : state && state !== "busy" ? /* @__PURE__ */ React.createElement("span", { title: state, style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--warn, #f59e0b)", flexShrink: 0 } }, T.addFailed || "failed") : btn(state === "busy" ? "\u2026" : T.dirAdd || "Add", add, state === "busy"));
|
|
2951
|
+
}
|
|
2952
|
+
function DiscoverTab({ T, kind, peers, meId, onAdd, onOpenChat }) {
|
|
2953
|
+
const [list, setList] = React.useState(null);
|
|
2954
|
+
const [err, setErr] = React.useState(null);
|
|
2955
|
+
const [q, setQ] = React.useState("");
|
|
2956
|
+
React.useEffect(() => {
|
|
2957
|
+
let dead = false;
|
|
2958
|
+
setList(null);
|
|
2959
|
+
setErr(null);
|
|
2960
|
+
fetch(kind === "recommended" ? "/api/discover-recommended" : "/api/discover-registered").then((r) => r.json()).then((d) => {
|
|
2961
|
+
if (dead)
|
|
2962
|
+
return;
|
|
2963
|
+
if (d.ok)
|
|
2964
|
+
setList(d.list || []);
|
|
2965
|
+
else
|
|
2966
|
+
setErr(d.error || "load failed");
|
|
2967
|
+
}).catch((e) => {
|
|
2968
|
+
if (!dead)
|
|
2969
|
+
setErr(String(e && e.message || e));
|
|
2970
|
+
});
|
|
2971
|
+
return () => {
|
|
2972
|
+
dead = true;
|
|
2973
|
+
};
|
|
2974
|
+
}, [kind]);
|
|
2975
|
+
const friendIds = React.useMemo(() => {
|
|
2976
|
+
const s = /* @__PURE__ */ new Set();
|
|
2977
|
+
for (const p of peers || []) {
|
|
2978
|
+
if (p.id)
|
|
2979
|
+
s.add(p.id);
|
|
2980
|
+
if (p.userId)
|
|
2981
|
+
s.add(p.userId);
|
|
2982
|
+
}
|
|
2983
|
+
return s;
|
|
2984
|
+
}, [peers]);
|
|
2985
|
+
const shown = React.useMemo(() => {
|
|
2986
|
+
if (!list)
|
|
2987
|
+
return [];
|
|
2988
|
+
const needle = q.trim().toLowerCase();
|
|
2989
|
+
if (!needle)
|
|
2990
|
+
return list;
|
|
2991
|
+
return list.filter((p) => (p.name || "").toLowerCase().includes(needle) || (p.ens || "").toLowerCase().includes(needle) || (p.userid || "").toLowerCase().includes(needle));
|
|
2992
|
+
}, [list, q]);
|
|
2993
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", alignItems: "center", gap: 10, padding: "10px 14px", borderBottom: "1px solid var(--line)", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "search", size: 15, stroke: 2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement(
|
|
2994
|
+
"input",
|
|
2995
|
+
{
|
|
2996
|
+
value: q,
|
|
2997
|
+
onChange: (e) => setQ(e.target.value),
|
|
2998
|
+
placeholder: T.dirSearch || "search name / userid",
|
|
2999
|
+
style: { flex: 1, background: "transparent", border: "none", outline: "none", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13 }
|
|
3000
|
+
}
|
|
3001
|
+
), list && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, shown.length, "/", list.length)), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minHeight: 0, overflowY: "auto" } }, err ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, (T.dirError || "Could not load the directory:") + " " + err) : !list ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : shown.length === 0 ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirEmpty || "nobody here") : shown.map((p) => /* @__PURE__ */ React.createElement(
|
|
3002
|
+
DkDirRow,
|
|
3003
|
+
{
|
|
3004
|
+
key: p.userid,
|
|
3005
|
+
p,
|
|
3006
|
+
T,
|
|
3007
|
+
isMe: !!meId && p.userid === meId,
|
|
3008
|
+
isFriend: friendIds.has(p.userid),
|
|
3009
|
+
onAdd,
|
|
3010
|
+
onOpenChat
|
|
3011
|
+
}
|
|
3012
|
+
))));
|
|
3013
|
+
}
|
|
2825
3014
|
const DK_FILE_RTC_KIND = "file";
|
|
2826
3015
|
const DK_FILE_RTC_CHUNK = 16 * 1024;
|
|
2827
3016
|
const DK_FILE_RTC_OPEN_TIMEOUT_MS = 2e4;
|
|
@@ -3645,7 +3834,17 @@ const STR = {
|
|
|
3645
3834
|
chat: "Chat",
|
|
3646
3835
|
network: "Network",
|
|
3647
3836
|
profile: "Profile",
|
|
3648
|
-
|
|
3837
|
+
recommended: "Discover",
|
|
3838
|
+
registered: "Names",
|
|
3839
|
+
dirAdd: "Add",
|
|
3840
|
+
dirSent: "requested",
|
|
3841
|
+
dirOpenChat: "chat",
|
|
3842
|
+
dirMe: "me",
|
|
3843
|
+
dirSearch: "search name / userid\u2026",
|
|
3844
|
+
dirLoading: "loading directory\u2026",
|
|
3845
|
+
dirEmpty: "nobody matches",
|
|
3846
|
+
dirError: "Could not load the directory:",
|
|
3847
|
+
addPlaceholder: "carrier address or name.beagles.eth\u2026",
|
|
3649
3848
|
add: "Add",
|
|
3650
3849
|
search: "search peers / ip\u2026",
|
|
3651
3850
|
addSending: "sending friend-request\u2026",
|
|
@@ -3716,6 +3915,20 @@ const STR = {
|
|
|
3716
3915
|
userId: "user id",
|
|
3717
3916
|
carrierAddr: "carrier address",
|
|
3718
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",
|
|
3719
3932
|
virtualIp: "virtual ip",
|
|
3720
3933
|
version: "version",
|
|
3721
3934
|
editProfile: "edit",
|
|
@@ -3746,7 +3959,17 @@ const STR = {
|
|
|
3746
3959
|
chat: "\u804A\u5929",
|
|
3747
3960
|
network: "\u7F51\u7EDC",
|
|
3748
3961
|
profile: "\u6211\u7684",
|
|
3749
|
-
|
|
3962
|
+
recommended: "\u63A8\u8350",
|
|
3963
|
+
registered: "\u540D\u5F55",
|
|
3964
|
+
dirAdd: "\u52A0\u597D\u53CB",
|
|
3965
|
+
dirSent: "\u5DF2\u8BF7\u6C42",
|
|
3966
|
+
dirOpenChat: "\u4F1A\u8BDD",
|
|
3967
|
+
dirMe: "\u6211",
|
|
3968
|
+
dirSearch: "\u641C\u7D22\u540D\u5B57 / userid\u2026",
|
|
3969
|
+
dirLoading: "\u6B63\u5728\u52A0\u8F7D\u540D\u5355\u2026",
|
|
3970
|
+
dirEmpty: "\u6CA1\u6709\u5339\u914D\u7684\u4EBA",
|
|
3971
|
+
dirError: "\u540D\u5355\u52A0\u8F7D\u5931\u8D25\uFF1A",
|
|
3972
|
+
addPlaceholder: "carrier \u5730\u5740\u6216 \u540D\u5B57.beagles.eth\u2026",
|
|
3750
3973
|
add: "\u6DFB\u52A0",
|
|
3751
3974
|
search: "\u641C\u7D22\u597D\u53CB / IP\u2026",
|
|
3752
3975
|
addSending: "\u6B63\u5728\u53D1\u9001\u597D\u53CB\u8BF7\u6C42\u2026",
|
|
@@ -3817,6 +4040,20 @@ const STR = {
|
|
|
3817
4040
|
userId: "\u7528\u6237 ID",
|
|
3818
4041
|
carrierAddr: "Carrier \u5730\u5740",
|
|
3819
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",
|
|
3820
4057
|
virtualIp: "\u865A\u62DF IP",
|
|
3821
4058
|
version: "\u7248\u672C",
|
|
3822
4059
|
editProfile: "\u7F16\u8F91",
|
|
@@ -4035,10 +4272,12 @@ function DkApp() {
|
|
|
4035
4272
|
const onOpenNet = () => setTab("network");
|
|
4036
4273
|
const nav = [
|
|
4037
4274
|
{ id: "chat", icon: "message", label: T.chat },
|
|
4275
|
+
{ id: "recommended", icon: "sparkles", label: T.recommended },
|
|
4276
|
+
{ id: "registered", icon: "at", label: T.registered },
|
|
4038
4277
|
{ id: "network", icon: "network", label: T.network },
|
|
4039
4278
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
4040
4279
|
];
|
|
4041
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
4280
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
4042
4281
|
TweakRadio,
|
|
4043
4282
|
{
|
|
4044
4283
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
package/dist/server.js
CHANGED
|
@@ -29,6 +29,88 @@ const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
|
|
|
29
29
|
// (which probes all exits on each poll) stays responsive over a lossy mesh.
|
|
30
30
|
const EXIT_PROBE_PORT = 8888;
|
|
31
31
|
const EXIT_PROBE_TIMEOUT_MS = 2000;
|
|
32
|
+
/** Server-side fetch for the discovery directories (leaderboard / ENS names),
|
|
33
|
+
* with a small TTL cache so tab switches don't hammer the upstreams. */
|
|
34
|
+
const DISCOVER_CACHE_TTL_MS = 5 * 60_000;
|
|
35
|
+
const discoverCache = new Map();
|
|
36
|
+
async function fetchDiscoverJson(url) {
|
|
37
|
+
const hit = discoverCache.get(url);
|
|
38
|
+
if (hit && Date.now() - hit.at < DISCOVER_CACHE_TTL_MS)
|
|
39
|
+
return hit.body;
|
|
40
|
+
const ctl = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => ctl.abort(), 10_000);
|
|
42
|
+
try {
|
|
43
|
+
const r = await fetch(url, { signal: ctl.signal });
|
|
44
|
+
if (!r.ok)
|
|
45
|
+
throw new Error(`upstream ${r.status}`);
|
|
46
|
+
const body = await r.json();
|
|
47
|
+
discoverCache.set(url, { at: Date.now(), body });
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
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
|
+
}
|
|
32
114
|
/** True only when the request came from the local machine. Used to gate the
|
|
33
115
|
* "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
|
|
34
116
|
* identity signing to other hosts. The popup always runs in the local user's
|
|
@@ -342,6 +424,116 @@ export function startBeagleServer(opts) {
|
|
|
342
424
|
});
|
|
343
425
|
return;
|
|
344
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
|
+
}
|
|
345
537
|
// Delete file/chat messages by id (removes their on-disk files too).
|
|
346
538
|
if (req.method === "POST" && url === "/api/file-delete") {
|
|
347
539
|
const body = await readBody(req);
|
|
@@ -619,6 +811,50 @@ export function startBeagleServer(opts) {
|
|
|
619
811
|
sendJson(res, 200, { friends });
|
|
620
812
|
return;
|
|
621
813
|
}
|
|
814
|
+
// People-discovery directories, proxied server-side: the browser can't
|
|
815
|
+
// fetch these origins (CORS), and the cache keeps us polite upstream.
|
|
816
|
+
// recommended = points leaderboard; registered = mobile-app ENS names.
|
|
817
|
+
if (req.method === "GET" && url === "/api/discover-recommended") {
|
|
818
|
+
try {
|
|
819
|
+
const raw = (await fetchDiscoverJson("https://points.beagle.chat/leaderboard"));
|
|
820
|
+
const list = (Array.isArray(raw) ? raw : [])
|
|
821
|
+
.map((e) => ({
|
|
822
|
+
userid: typeof e.account === "string" ? e.account : "",
|
|
823
|
+
address: typeof e.carrier_address === "string" ? e.carrier_address : "",
|
|
824
|
+
name: (typeof e.name === "string" && e.name) || (typeof e.ens_name === "string" && e.ens_name) || "",
|
|
825
|
+
avatar: typeof e.avatar === "string" && /^https:\/\//.test(e.avatar) ? e.avatar : null,
|
|
826
|
+
points: typeof e.points === "number" ? e.points : null,
|
|
827
|
+
}))
|
|
828
|
+
.filter((e) => e.userid && e.address);
|
|
829
|
+
sendJson(res, 200, { ok: true, list });
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
sendJson(res, 502, { ok: false, error: String(err?.message ?? err) });
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (req.method === "GET" && url === "/api/discover-registered") {
|
|
837
|
+
try {
|
|
838
|
+
const raw = (await fetchDiscoverJson("https://ens-gateway.beaglechat.workers.dev/names"));
|
|
839
|
+
const list = Object.entries(raw && typeof raw === "object" ? raw : {})
|
|
840
|
+
.map(([ens, v]) => {
|
|
841
|
+
const t = v?.texts ?? {};
|
|
842
|
+
return {
|
|
843
|
+
userid: t.carrierUserId ?? "",
|
|
844
|
+
address: t.carrierAddress ?? "",
|
|
845
|
+
name: t.displayName || ens,
|
|
846
|
+
ens,
|
|
847
|
+
description: t.description || null,
|
|
848
|
+
};
|
|
849
|
+
})
|
|
850
|
+
.filter((e) => e.userid && e.address);
|
|
851
|
+
sendJson(res, 200, { ok: true, list });
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
sendJson(res, 502, { ok: false, error: String(err?.message ?? err) });
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
622
858
|
// One bootstrap call for the desktop UI: composes the design's DK_*
|
|
623
859
|
// shapes (me / peers / requests / exits) from diag + friends-list +
|
|
624
860
|
// ipam + routes, so the client data layer is a single poll.
|
|
@@ -990,6 +1226,31 @@ export function startBeagleServer(opts) {
|
|
|
990
1226
|
}
|
|
991
1227
|
if (req.method === "POST" && url === "/api/add") {
|
|
992
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
|
+
}
|
|
993
1254
|
// Accept a 44-char userid too: the address is userid's pubkey plus a
|
|
994
1255
|
// derivable suffix (nospam=0 network-wide + checksum), and users
|
|
995
1256
|
// frequently have only the userid — it's what every other surface
|
package/package.json
CHANGED