From cdd5fd52e981c4c59643e7dee705b6c55acae68e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 8 Oct 2026 01:12:01 +0200 Subject: 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 --- .../src/meshbay_hub/static/transport-roster.js | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport-roster.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-roster.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-roster.js index cac3b52..d2e8c94 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport-roster.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-roster.js @@ -102,6 +102,36 @@ async function _writePinnedAccount(nodePk, userId, keys) { } catch { /* private window, or storage refused — one more "first sight" */ } } +/** + * How `devicePk` stands against what is pinned for its account (`known`, null + * at first sight) and what the roster lists for it (`entry`): `{ status, pin }`, + * `pin` being the keys to keep if that answer stands. Decides and writes + * nothing, so a verdict reached on a stale roster can be taken again on a fresh + * one without having pinned half a set first. The statuses are + * `accountDeviceStatus`'s. + */ +function _judgeDevice(known, entry, devicePk) { + if (known && known.includes(devicePk)) return { status: 'pinned', pin: null }; + 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. + const pin = entry ? entry.all : null; + return { status: entry && entry.all.includes(devicePk) ? 'first' : 'changed', pin }; + } + 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. + return { status: 'linked', pin: [...new Set([...known, devicePk])] }; + } + return { status: 'changed', pin: null }; +} + /** * A wire payload as text. * -- cgit v1.2.3