aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-08 01:12:01 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-08 01:12:01 +0200
commitcdd5fd52e981c4c59643e7dee705b6c55acae68e (patch)
tree3aea907ab1f494b8e8fbecf72ef16edcf4f6ae52 /packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
parented0c680790950354f15fb5835e1d7b213efa1bf8 (diff)
downloadmeshbay-cdd5fd52e981c4c59643e7dee705b6c55acae68e.tar.gz
fix(hub): re-read the roster before saying a key changed
A member invited after the roster was read showed "key changed" on each message until a reload. Read it again once per account and device on the connection, shared by concurrent messages, and pin only the final verdict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js51
1 files changed, 31 insertions, 20 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
index f079f80..3d56f69 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
@@ -68,28 +68,39 @@ extendTransport(class {
return 'unknown';
}
const known = await _readPinnedAccount(this.nodePk, userId);
- const entry = roster.byAccount.get(userId);
- if (known && known.includes(devicePk)) return 'pinned';
- if (!known) {
- // First sight, so **everything the node says** is pinned — not only what
- // a chain reaches. There is nothing to compare against yet: that is what
- // trust-on-first-use means, and pinning only the verified subset would
- // raise "key changed" on a legitimate second device whose
- // countersignature simply predates it being kept. What TOFU buys is that
- // a substitution *later* is visible; it cannot buy anything now.
- if (entry) await _writePinnedAccount(this.nodePk, userId, entry.all);
- return entry && entry.all.includes(devicePk) ? 'first' : 'changed';
+ let verdict = _judgeDevice(known, roster.byAccount.get(userId), devicePk);
+ // The roster is this connection's copy, read once. Somebody who joined the
+ // group, or added a device, since it was read is absent from it, and their
+ // first message raised "key changed" — found live, twice in one
+ // conversation, on a member invited after the page was opened. So the
+ // roster is read again before that is said, once per account and device
+ // on this connection: a node substituting keys gets one more read for each
+ // key it makes up, and the same answer.
+ if (verdict.status === 'changed') {
+ try {
+ roster = await this._rosterRecheckedFor(userId, devicePk);
+ verdict = _judgeDevice(known, roster.byAccount.get(userId), devicePk);
+ } catch { /* the verdict on the copy we had stands */ }
}
- if (entry && entry.verified.includes(devicePk)
- && entry.chain.get(devicePk)
- && known.includes(entry.chain.get(devicePk))) {
- // Countersigned by a key we already trust for this account: a second
- // device of someone we know, admitted without anybody comparing digits.
- await _writePinnedAccount(this.nodePk, userId,
- [...new Set([...known, devicePk])]);
- return 'linked';
+ if (verdict.pin) await _writePinnedAccount(this.nodePk, userId, verdict.pin);
+ return verdict.status;
+ }
+
+ /**
+ * The roster as re-read for this key, read once per connection. Held as the
+ * promise, not as "done": live messages are opened concurrently, and two from
+ * a device that just joined must both wait for the one read, not have the
+ * second answer "changed" while the first is still asking.
+ */
+ _rosterRecheckedFor(userId, devicePk) {
+ if (!this._rosterRechecks) this._rosterRechecks = new Map();
+ const key = `${userId} ${devicePk}`;
+ let read = this._rosterRechecks.get(key);
+ if (!read) {
+ read = this.groupRoster({ fresh: true });
+ this._rosterRechecks.set(key, read);
}
- return 'changed';
+ return read;
}
/**