@decentnetwork/lan 0.1.284 → 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.
@@ -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;
@@ -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. */
@@ -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
@@ -458,6 +459,7 @@ export class DaemonServer {
458
459
  void (async () => {
459
460
  try {
460
461
  await this.peerManager.sendText(userid, text);
462
+ this.friendMeta?.markConfirmed(userid); // it landed: mutual
461
463
  this.messageStore?.setStatus(userid, msg.id, undefined); // delivered
462
464
  }
463
465
  catch (e) {
@@ -491,6 +493,11 @@ export class DaemonServer {
491
493
  // The build-default nickname is useless (every node sends it) —
492
494
  // treat it as no-name so the UI falls back to alias/userid.
493
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);
494
501
  return {
495
502
  userid: uid,
496
503
  // Full Carrier address (pubkey+nospam+checksum) — the form a NEW
@@ -499,7 +506,7 @@ export class DaemonServer {
499
506
  address: f.address,
500
507
  alias: meta?.alias,
501
508
  name: meta?.alias || realName || uid,
502
- status: f.status,
509
+ status: unproven ? "requested" : f.status,
503
510
  lastSeen: f.lastSeen,
504
511
  pinned: meta?.pinned ?? false,
505
512
  lastMessage: lastMsg ? { dir: lastMsg.dir, text: lastMsg.text, ts: lastMsg.ts } : undefined,
@@ -1146,6 +1153,7 @@ export class DaemonServer {
1146
1153
  };
1147
1154
  // Chat: log incoming Carrier text messages (packet 64) for the UI.
1148
1155
  this.peerManager.on("message", (pubkey, text, via) => {
1156
+ this.friendMeta?.markConfirmed(pubkey); // they reached us: mutual
1149
1157
  this.logChat(pubkey, "in", text, via);
1150
1158
  });
1151
1159
  // Presence: push to IPC subscribers so the TUI/UI reflects online/offline
@@ -1212,6 +1220,7 @@ export class DaemonServer {
1212
1220
  }
1213
1221
  });
1214
1222
  this.peerManager.on("file-complete", (p) => {
1223
+ this.friendMeta?.markConfirmed(p.friendId);
1215
1224
  if (p.sending || !p.data) {
1216
1225
  if (p.sending) {
1217
1226
  // The receiver has ACKed the whole file — now it's truly delivered.
@@ -1634,6 +1643,32 @@ export class DaemonServer {
1634
1643
  * text and queued files, oldest first. Called when a friend reconnects.
1635
1644
  * Stops early if the link drops again, leaving the remainder queued for the
1636
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
+ }
1637
1672
  async flushOutbox(userid) {
1638
1673
  if (this.flushingOutbox.has(userid))
1639
1674
  return;
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.284";
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.284",
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",