@decentnetwork/beagle 0.1.18 → 0.1.20
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 +115 -4
- package/dist/server.js +195 -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, 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")));
|
|
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", "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)));
|
|
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: "
|
|
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
|
+
ensUnbind: "unbind",
|
|
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: "
|
|
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
|
+
ensUnbind: "\u89E3\u7ED1",
|
|
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