@decentnetwork/lan 0.1.283 → 0.1.285
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/daemon/friend-meta.d.ts +15 -0
- package/dist/daemon/friend-meta.js +12 -0
- package/dist/daemon/server.d.ts +11 -0
- package/dist/daemon/server.js +75 -15
- package/dist/ui/desktop/app.js +1 -1
- package/package.json +1 -1
|
@@ -14,6 +14,18 @@ export interface FriendMeta {
|
|
|
14
14
|
pinned?: boolean;
|
|
15
15
|
/** ts of the newest message the user has seen — drives unread counts. */
|
|
16
16
|
lastReadTs?: number;
|
|
17
|
+
/**
|
|
18
|
+
* When we first had PROOF this friendship is mutual — something arrived from
|
|
19
|
+
* them, or a send to them landed. Both need a two-way session, which needs
|
|
20
|
+
* them to have accepted.
|
|
21
|
+
*
|
|
22
|
+
* The SDK clears its own "requested" state on a dhtpk_update, which it reads
|
|
23
|
+
* as acceptance. It is not: that packet says the peer exists and is
|
|
24
|
+
* reachable, nothing about a human pressing Accept. Trusting it showed a
|
|
25
|
+
* peer as online before they had accepted, while every message to them
|
|
26
|
+
* queued — because there is no session to someone who has not added you.
|
|
27
|
+
*/
|
|
28
|
+
confirmedAt?: number;
|
|
17
29
|
addedAt: number;
|
|
18
30
|
}
|
|
19
31
|
export declare class FriendMetaStore {
|
|
@@ -29,6 +41,9 @@ export declare class FriendMetaStore {
|
|
|
29
41
|
ensure(userid: string): FriendMeta;
|
|
30
42
|
setAlias(userid: string, alias: string | undefined): void;
|
|
31
43
|
markRead(userid: string, ts?: number): void;
|
|
44
|
+
/** Record proof the friendship is mutual. Idempotent; first proof wins. */
|
|
45
|
+
markConfirmed(userid: string, ts?: number): boolean;
|
|
46
|
+
isConfirmed(userid: string): boolean;
|
|
32
47
|
setPinned(userid: string, pinned: boolean): void;
|
|
33
48
|
remove(userid: string): void;
|
|
34
49
|
private scheduleSave;
|
|
@@ -59,6 +59,18 @@ export class FriendMetaStore {
|
|
|
59
59
|
this.scheduleSave();
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
/** Record proof the friendship is mutual. Idempotent; first proof wins. */
|
|
63
|
+
markConfirmed(userid, ts = Date.now()) {
|
|
64
|
+
const m = this.ensure(userid);
|
|
65
|
+
if (m.confirmedAt)
|
|
66
|
+
return false;
|
|
67
|
+
m.confirmedAt = ts;
|
|
68
|
+
this.scheduleSave();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
isConfirmed(userid) {
|
|
72
|
+
return !!this.byUserid.get(userid)?.confirmedAt;
|
|
73
|
+
}
|
|
62
74
|
setPinned(userid, pinned) {
|
|
63
75
|
const m = this.ensure(userid);
|
|
64
76
|
m.pinned = pinned || undefined;
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -135,6 +135,17 @@ export declare class DaemonServer {
|
|
|
135
135
|
* text and queued files, oldest first. Called when a friend reconnects.
|
|
136
136
|
* Stops early if the link drops again, leaving the remainder queued for the
|
|
137
137
|
* next reconnect (true store-and-forward). Re-entrancy-guarded per peer. */
|
|
138
|
+
/**
|
|
139
|
+
* One-time backfill so upgrading does not re-open settled friendships.
|
|
140
|
+
*
|
|
141
|
+
* requestedAt stays on a friend record forever, so the new "unproven"
|
|
142
|
+
* rule would mark every friend we ever ASKED — i.e. most of them — as
|
|
143
|
+
* "requested" again until the next message. An inbound message is the same
|
|
144
|
+
* proof the live rule uses: they could only have sent it over a session that
|
|
145
|
+
* required them to accept us. So anyone we have already heard from is
|
|
146
|
+
* confirmed at startup, and only genuinely unaccepted requests stay pending.
|
|
147
|
+
*/
|
|
148
|
+
private backfillConfirmedFriends;
|
|
138
149
|
private flushOutbox;
|
|
139
150
|
/** Per-peer timestamp of the last self-heal friend-request re-send, so
|
|
140
151
|
* the watchdog escalates at most once per SELF_HEAL_REFRIEND_MS. */
|
package/dist/daemon/server.js
CHANGED
|
@@ -385,6 +385,7 @@ export class DaemonServer {
|
|
|
385
385
|
// On-disk chat + friend-metadata stores (survive restarts; back the UI).
|
|
386
386
|
this.messageStore = new MessageStore(resolve(this.configDir, "messages.json"));
|
|
387
387
|
this.friendMeta = new FriendMetaStore(resolve(this.configDir, "friends-meta.json"));
|
|
388
|
+
this.backfillConfirmedFriends();
|
|
388
389
|
this.outboxDir = resolve(this.configDir, "outbox");
|
|
389
390
|
this.reconcileOutboxOnStart();
|
|
390
391
|
// Start IPC as soon as the peer is up. The CLI uses it to drive
|
|
@@ -393,7 +394,11 @@ export class DaemonServer {
|
|
|
393
394
|
this.ipcEvents.setMaxListeners(50);
|
|
394
395
|
this.ipcServer = new IpcServer(ipcSocketPath(this.config.carrier.dataDir), {
|
|
395
396
|
friendRequest: async (address, hello) => {
|
|
396
|
-
|
|
397
|
+
// sendFriendRequest waits for our own announce to store and then
|
|
398
|
+
// walks the DHT toward the target — tens of seconds is normal, and
|
|
399
|
+
// none of it changes what the caller should do. Awaiting it made
|
|
400
|
+
// "add" look like a dead button for the whole walk.
|
|
401
|
+
void this.peerManager.sendFriendRequest(address, hello).catch((e) => this.logger.warn(`friend-request to ${String(address).slice(0, 8)} failed: ${e.message}`));
|
|
397
402
|
},
|
|
398
403
|
friendsPending: async () => {
|
|
399
404
|
const entries = this.pendingFriends?.list() ?? [];
|
|
@@ -412,7 +417,14 @@ export class DaemonServer {
|
|
|
412
417
|
const entry = this.pendingFriends?.remove(userid);
|
|
413
418
|
if (!entry)
|
|
414
419
|
throw new Error(`No pending friend-request for userid ${userid}`);
|
|
415
|
-
|
|
420
|
+
// Answer as soon as the entry is gone from pending — that is the part
|
|
421
|
+
// the UI renders. The node call is effectively local (its session
|
|
422
|
+
// attempt is already fire-and-forget), so waiting on it only delayed
|
|
423
|
+
// the click. Put the entry back if it does refuse.
|
|
424
|
+
void this.peerManager.acceptFriendRequest(entry.pubkey).catch((e) => {
|
|
425
|
+
this.pendingFriends?.add(entry);
|
|
426
|
+
this.logger.warn(`accept ${userid.slice(0, 8)} failed: ${e.message}`);
|
|
427
|
+
});
|
|
416
428
|
},
|
|
417
429
|
friendsReject: async (userid) => {
|
|
418
430
|
const entry = this.pendingFriends?.remove(userid);
|
|
@@ -423,23 +435,38 @@ export class DaemonServer {
|
|
|
423
435
|
chatSend: async (userid, text) => {
|
|
424
436
|
if (!text)
|
|
425
437
|
return;
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
438
|
+
// Record the message BEFORE any network attempt, and return without
|
|
439
|
+
// waiting for the wire.
|
|
440
|
+
//
|
|
441
|
+
// The offline branch already did this. The ONLINE branch awaited
|
|
442
|
+
// sendText and only logged afterwards — and sendText is not quick:
|
|
443
|
+
// with the ack path it retries for up to 15s. So a message to an
|
|
444
|
+
// ONLINE friend did not appear in the sender's own thread until
|
|
445
|
+
// delivery finished, while a message to an offline one appeared at
|
|
446
|
+
// once. The faster the peer, the less you noticed; the slower, the
|
|
447
|
+
// more it looked like the app had ignored you.
|
|
448
|
+
//
|
|
449
|
+
// Same rule the embedded host and the browser client already follow:
|
|
450
|
+
// a delivery failure is a STATE on the message, never its absence.
|
|
451
|
+
const msg = this.messageStore?.append(userid, "out", text, Date.now(), "queued");
|
|
452
|
+
this.friendMeta?.ensure(userid);
|
|
453
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
454
|
+
if (!msg || !this.peerManager?.isFriendOnline(userid)) {
|
|
455
|
+
this.logger.info(`Queued text for ${userid.slice(0, 8)} (delivers on reconnect)`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Deliver in the background; the reconnect flush covers a failure.
|
|
459
|
+
void (async () => {
|
|
430
460
|
try {
|
|
431
461
|
await this.peerManager.sendText(userid, text);
|
|
432
|
-
this.
|
|
433
|
-
|
|
462
|
+
this.friendMeta?.markConfirmed(userid); // it landed: mutual
|
|
463
|
+
this.messageStore?.setStatus(userid, msg.id, undefined); // delivered
|
|
434
464
|
}
|
|
435
465
|
catch (e) {
|
|
436
|
-
this.logger.warn(`sendText to ${userid.slice(0, 8)} failed,
|
|
466
|
+
this.logger.warn(`sendText to ${userid.slice(0, 8)} failed, stays queued: ${e.message}`);
|
|
437
467
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
this.friendMeta?.ensure(userid);
|
|
441
|
-
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
442
|
-
this.logger.info(`Queued text for offline ${userid.slice(0, 8)} (delivers on reconnect)`);
|
|
468
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
469
|
+
})();
|
|
443
470
|
},
|
|
444
471
|
chatLogLocal: async (userid, dir, text) => {
|
|
445
472
|
if (!text)
|
|
@@ -466,6 +493,11 @@ export class DaemonServer {
|
|
|
466
493
|
// The build-default nickname is useless (every node sends it) —
|
|
467
494
|
// treat it as no-name so the UI falls back to alias/userid.
|
|
468
495
|
const realName = f.name && f.name !== "@decentnetwork/peer" ? f.name : undefined;
|
|
496
|
+
// A peer WE asked stays "requested" until proof arrives, whatever
|
|
497
|
+
// the SDK's own status says. Peers we did not ask (inbound
|
|
498
|
+
// requests we accepted) have no requestedAt, so they are
|
|
499
|
+
// unaffected — as is every friend already confirmed.
|
|
500
|
+
const unproven = !!f.requestedAt && !this.friendMeta?.isConfirmed(uid);
|
|
469
501
|
return {
|
|
470
502
|
userid: uid,
|
|
471
503
|
// Full Carrier address (pubkey+nospam+checksum) — the form a NEW
|
|
@@ -474,7 +506,7 @@ export class DaemonServer {
|
|
|
474
506
|
address: f.address,
|
|
475
507
|
alias: meta?.alias,
|
|
476
508
|
name: meta?.alias || realName || uid,
|
|
477
|
-
status: f.status,
|
|
509
|
+
status: unproven ? "requested" : f.status,
|
|
478
510
|
lastSeen: f.lastSeen,
|
|
479
511
|
pinned: meta?.pinned ?? false,
|
|
480
512
|
lastMessage: lastMsg ? { dir: lastMsg.dir, text: lastMsg.text, ts: lastMsg.ts } : undefined,
|
|
@@ -1121,6 +1153,7 @@ export class DaemonServer {
|
|
|
1121
1153
|
};
|
|
1122
1154
|
// Chat: log incoming Carrier text messages (packet 64) for the UI.
|
|
1123
1155
|
this.peerManager.on("message", (pubkey, text, via) => {
|
|
1156
|
+
this.friendMeta?.markConfirmed(pubkey); // they reached us: mutual
|
|
1124
1157
|
this.logChat(pubkey, "in", text, via);
|
|
1125
1158
|
});
|
|
1126
1159
|
// Presence: push to IPC subscribers so the TUI/UI reflects online/offline
|
|
@@ -1187,6 +1220,7 @@ export class DaemonServer {
|
|
|
1187
1220
|
}
|
|
1188
1221
|
});
|
|
1189
1222
|
this.peerManager.on("file-complete", (p) => {
|
|
1223
|
+
this.friendMeta?.markConfirmed(p.friendId);
|
|
1190
1224
|
if (p.sending || !p.data) {
|
|
1191
1225
|
if (p.sending) {
|
|
1192
1226
|
// The receiver has ACKed the whole file — now it's truly delivered.
|
|
@@ -1609,6 +1643,32 @@ export class DaemonServer {
|
|
|
1609
1643
|
* text and queued files, oldest first. Called when a friend reconnects.
|
|
1610
1644
|
* Stops early if the link drops again, leaving the remainder queued for the
|
|
1611
1645
|
* next reconnect (true store-and-forward). Re-entrancy-guarded per peer. */
|
|
1646
|
+
/**
|
|
1647
|
+
* One-time backfill so upgrading does not re-open settled friendships.
|
|
1648
|
+
*
|
|
1649
|
+
* requestedAt stays on a friend record forever, so the new "unproven"
|
|
1650
|
+
* rule would mark every friend we ever ASKED — i.e. most of them — as
|
|
1651
|
+
* "requested" again until the next message. An inbound message is the same
|
|
1652
|
+
* proof the live rule uses: they could only have sent it over a session that
|
|
1653
|
+
* required them to accept us. So anyone we have already heard from is
|
|
1654
|
+
* confirmed at startup, and only genuinely unaccepted requests stay pending.
|
|
1655
|
+
*/
|
|
1656
|
+
backfillConfirmedFriends() {
|
|
1657
|
+
if (!this.messageStore || !this.friendMeta)
|
|
1658
|
+
return;
|
|
1659
|
+
let marked = 0;
|
|
1660
|
+
const all = this.messageStore.history();
|
|
1661
|
+
for (const [peer, msgs] of Object.entries(all)) {
|
|
1662
|
+
if (this.friendMeta.isConfirmed(peer))
|
|
1663
|
+
continue;
|
|
1664
|
+
if (msgs.some((m) => m.dir === "in")) {
|
|
1665
|
+
this.friendMeta.markConfirmed(peer);
|
|
1666
|
+
marked++;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
if (marked)
|
|
1670
|
+
this.logger.info(`Confirmed ${marked} existing friend(s) from chat history`);
|
|
1671
|
+
}
|
|
1612
1672
|
async flushOutbox(userid) {
|
|
1613
1673
|
if (this.flushingOutbox.has(userid))
|
|
1614
1674
|
return;
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.285";
|
|
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"/>',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.285",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|