@decentnetwork/beagle 0.1.91 → 0.1.93

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.91";
1
+ window.__DK_UI_VERSION="0.1.93";
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/beagle-ui/dist/beagle-ui.js
@@ -2443,7 +2443,7 @@ ${peer.address}`
2443
2443
  const register = () => {
2444
2444
  const l = label.trim();
2445
2445
  if (l)
2446
- post("/api/ens-register", { name: l, displayName: me.name });
2446
+ post("/api/ens-register", { name: l, displayName: me.name, description: me.description || "" });
2447
2447
  };
2448
2448
  const bind = (chain, address) => post("/api/ens-bind-wallet", { chain, address });
2449
2449
  const bindEth = async () => {
package/dist/server.js CHANGED
@@ -75,6 +75,73 @@ async function ensFetchJson(path) {
75
75
  clearTimeout(timer);
76
76
  }
77
77
  }
78
+ /** requestedAt / acceptedAt per friend, read from the SDK's own friend store
79
+ * (`<keyFile>.friends.json`, next to the identity). The daemon's friends-list
80
+ * reports neither for a request we sent and only acceptedAt (as lastSeen)
81
+ * otherwise, so a friend just added had no stamp at all and sorted to the
82
+ * bottom — the third time this was reported. The store is the source of
83
+ * truth both backends write, and it is a file in our own config dir, so
84
+ * read it. Cached by mtime: the list is polled every 2.5s per tab. */
85
+ let friendStampsCache = null;
86
+ function friendStoreStamps(keyFile) {
87
+ const empty = new Map();
88
+ if (!keyFile)
89
+ return empty;
90
+ const path = `${keyFile}.friends.json`;
91
+ try {
92
+ const st = statSync(path);
93
+ if (friendStampsCache && friendStampsCache.mtimeMs === st.mtimeMs)
94
+ return friendStampsCache.map;
95
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
96
+ const recs = Array.isArray(raw)
97
+ ? raw
98
+ : raw && typeof raw === "object" && Array.isArray(raw.friends)
99
+ ? raw.friends
100
+ : Object.values(raw ?? {});
101
+ const map = new Map();
102
+ for (const r of recs) {
103
+ if (!r || typeof r !== "object")
104
+ continue;
105
+ const id = r.userid || r.pubkey;
106
+ if (!id)
107
+ continue;
108
+ map.set(id, {
109
+ requestedAt: typeof r.requestedAt === "number" ? r.requestedAt : undefined,
110
+ acceptedAt: typeof r.acceptedAt === "number" ? r.acceptedAt : undefined,
111
+ });
112
+ }
113
+ friendStampsCache = { mtimeMs: st.mtimeMs, map };
114
+ return map;
115
+ }
116
+ catch {
117
+ // Unreadable or absent (a fresh install has no friends yet): whatever we
118
+ // last read, else nothing — the list still renders, just without stamps.
119
+ return friendStampsCache?.map ?? empty;
120
+ }
121
+ }
122
+ /** The record beagle writes for its own name: what the profile page shows
123
+ * (displayName, description) on top of whatever the record already held —
124
+ * wallet binds, socials, avatar, referee — so a profile edit never strips a
125
+ * bind. Shared by /api/ens-register and /api/set-profile; the two used to
126
+ * build it separately, and only register did, which is why a status message
127
+ * edited in Profile never reached https://app.beagle.chat/<name>. */
128
+ function ensProfileRecord(identity, name, carried, displayName, description) {
129
+ return {
130
+ name,
131
+ owner: identity.userid,
132
+ addresses: carried?.addresses ?? {},
133
+ texts: {
134
+ ...(carried?.texts ?? {}),
135
+ description,
136
+ displayName,
137
+ carrierAddress: identity.address,
138
+ carrierUserId: identity.userid,
139
+ },
140
+ referee: carried?.referee ?? "",
141
+ nft: carried?.nft ?? "",
142
+ nftid: carried?.nftid ?? 0,
143
+ };
144
+ }
78
145
  /** Sign a name record with the Carrier identity and POST it to the gateway.
79
146
  * The record itself (as a JSON string) is what gets signed; the gateway
80
147
  * re-parses `signature.message` and verifies against `owner` inside it. */
@@ -1219,24 +1286,15 @@ export function startBeagleServer(opts) {
1219
1286
  const mine = existing ?? (await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`));
1220
1287
  const carried = mine && mine.owner === identity.userid ? mine : null;
1221
1288
  const displayName = (typeof body.displayName === "string" && body.displayName.trim().slice(0, 64)) || label;
1289
+ // The description IS the Profile status message. It used to fall
1290
+ // through to "<name>'s Beagle Chat Profile", a placeholder that then
1291
+ // sat on the public page as if the person had written it.
1292
+ const node = (diag.ok ? diag.data?.node : null) ?? {};
1222
1293
  const description = (typeof body.description === "string" && body.description.trim().slice(0, 200)) ||
1294
+ (typeof node.statusMessage === "string" && node.statusMessage.trim().slice(0, 200)) ||
1223
1295
  carried?.texts?.description ||
1224
- `${displayName}'s Beagle Chat Profile`;
1225
- const r = await ensSignAndSet(opts.call, {
1226
- name: fullName,
1227
- owner: identity.userid,
1228
- addresses: carried?.addresses ?? {},
1229
- texts: {
1230
- ...(carried?.texts ?? {}),
1231
- description,
1232
- displayName,
1233
- carrierAddress: identity.address,
1234
- carrierUserId: identity.userid,
1235
- },
1236
- referee: carried?.referee ?? "",
1237
- nft: carried?.nft ?? "",
1238
- nftid: carried?.nftid ?? 0,
1239
- });
1296
+ "";
1297
+ const r = await ensSignAndSet(opts.call, ensProfileRecord({ userid: identity.userid, address: identity.address }, fullName, carried, displayName, description));
1240
1298
  sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, name: fullName } : { ok: false, error: r.error });
1241
1299
  return;
1242
1300
  }
@@ -1952,10 +2010,12 @@ export function startBeagleServer(opts) {
1952
2010
  beagleVer: beagleVersion(),
1953
2011
  };
1954
2012
  const fl = (flist.ok ? (flist.data?.friends ?? []) : []);
2013
+ const stamps = friendStoreStamps(opts.callIce?.keyFile);
1955
2014
  const peers = fl.map((f) => {
1956
2015
  const uid = f.userid ?? "";
1957
2016
  const sess = sessByUserid.get(uid);
1958
2017
  const status = f.status;
2018
+ const stamp = stamps.get(uid);
1959
2019
  const online = status === "connected" || status === "online";
1960
2020
  const realName = typeof f.name === "string" && f.name !== uid ? f.name : undefined;
1961
2021
  const lm = f.lastMessage;
@@ -1968,10 +2028,12 @@ export function startBeagleServer(opts) {
1968
2028
  address: f.address || "",
1969
2029
  online,
1970
2030
  // Friendship not yet accepted by the other side. Only trusted when
1971
- // the backend states it explicitly (embedded does); a daemon
1972
- // backend that doesn't report it defaults to established, so the
1973
- // banner can never wrongly appear on an old backend.
1974
- pending: f.accepted === false,
2031
+ // the backend states it explicitly: the embedded host's `accepted`,
2032
+ // or the SDK status "requested" that the daemon passes through
2033
+ // (the SDK demotes it the moment the other side proves itself).
2034
+ // Anything else defaults to established, so the banner can never
2035
+ // wrongly appear on an old backend.
2036
+ pending: f.accepted === false || status === "requested",
1975
2037
  via: online ? viaFromTransport(sess?.transport) : null,
1976
2038
  ping: null,
1977
2039
  ip: ipByUserid.get(uid) ?? "",
@@ -1991,11 +2053,13 @@ export function startBeagleServer(opts) {
1991
2053
  // The sidebar ranks a friend by the newest of these and the last
1992
2054
  // message, so someone you just accepted — or just added — lands
1993
2055
  // at the top like a fresh message would, not in a no-history
1994
- // group at the bottom. A daemon backend reports only lastSeen
1995
- // (its acceptedAt); requestedAt is embedded-only and reads 0
1996
- // elsewhere.
1997
- acceptedAt: f.lastSeen ?? 0,
1998
- requestedAt: f.requestedAt ?? 0,
2056
+ // group at the bottom. The backend's own fields first; the SDK's
2057
+ // friend store on disk fills what the backend does not report — a
2058
+ // daemon backend gives only lastSeen (its acceptedAt) and nothing
2059
+ // for a request we sent, which left a just-added friend with no
2060
+ // stamp at all.
2061
+ acceptedAt: f.lastSeen ?? stamp?.acceptedAt ?? 0,
2062
+ requestedAt: f.requestedAt ?? stamp?.requestedAt ?? 0,
1999
2063
  wire: "163",
2000
2064
  avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
2001
2065
  // What they advertised over Carrier wins over the directory: it is
@@ -2257,6 +2321,31 @@ export function startBeagleServer(opts) {
2257
2321
  const { name, description, punkId } = await readBody(req);
2258
2322
  const r = await opts.call({ op: "set-profile", name, description, punkId });
2259
2323
  sendJson(res, r.ok ? 200 : 400, r);
2324
+ // Also the public record, when this identity registered a beagles.eth
2325
+ // name here: displayName and description on the record ARE the
2326
+ // profile name and status message, and editing them in Profile used
2327
+ // to change only what Carrier advertises, never the page. Best
2328
+ // effort, after the reply: the gateway being down must not turn a
2329
+ // profile save into a failure. A punk-only call (the avatar advertise
2330
+ // on poll) carries neither field and leaves the record alone.
2331
+ if (r.ok && (typeof name === "string" || typeof description === "string")) {
2332
+ void (async () => {
2333
+ const diag = await opts.call({ op: "diag" });
2334
+ const identity = (diag.ok ? diag.data?.identity : null) ?? {};
2335
+ if (!identity.userid || !identity.address)
2336
+ return;
2337
+ const mine = await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`);
2338
+ if (!mine || mine.owner !== identity.userid)
2339
+ return; // not registered here — nothing to sign
2340
+ const displayName = (typeof name === "string" && name.trim().slice(0, 64)) ||
2341
+ mine.texts?.displayName ||
2342
+ mine.name.replace(/\.beagles\.eth$/, "");
2343
+ const desc = typeof description === "string" ? description.trim().slice(0, 200) : (mine.texts?.description ?? "");
2344
+ const w = await ensSignAndSet(opts.call, ensProfileRecord({ userid: identity.userid, address: identity.address }, mine.name, mine, displayName, desc));
2345
+ if (!w.ok)
2346
+ console.warn(`[ens] profile saved, but the ${mine.name} record was not updated: ${w.error}`);
2347
+ })().catch((e) => console.warn(`[ens] record update after profile save failed: ${e.message}`));
2348
+ }
2260
2349
  return;
2261
2350
  }
2262
2351
  if (req.method === "GET" && url === "/api/routes") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.91",
3
+ "version": "0.1.93",
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",