diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 90 |
1 files changed, 82 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 0b2fed5..d90c072 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -174,6 +174,11 @@ function trace(event, data) { window.MeshBayTrace = { enabled: traceEnabled, + // So a module that is not this one can write to the same buffer. The + // composer's own state is the half of the "chat hangs, the textbox is dead" + // report transport.js cannot see, and it belongs in the same timeline as the + // channel events it has to be read against. + record: trace, dump() { try { return JSON.parse(localStorage.getItem(TRACE_LOG_KEY) || '[]'); } catch { return []; } }, @@ -482,6 +487,12 @@ class MeshBayTransport { this._inReconnectAttempt = false; this._onReconnected = null; this._onNeedToken = null; + // Which device key THIS connection has identified itself to the node with. + // Empty means "not identified": nothing can be sealed, so nothing can be + // posted to chat. Written only through _setDevicePk, which is what makes + // the change visible to a reader — see onDeviceIdentity. + this.devicePk = ''; + this._onDeviceIdentity = null; // Cuts the backoff wait short the moment the page is foregrounded again — // found live to matter: a screen lock throttles the tab's own timers // along with everything else, so a backoff already counting down when the @@ -581,6 +592,18 @@ class MeshBayTransport { // handshake, so a consumer with something mid-flight on the old channel — // today only the video player — can pick back up rather than sit dead. set onReconnected(fn) { this._onReconnected = fn; } + + /** + * Told whenever this connection's device identity changes — including to + * *nothing*, which is the case that mattered. + * + * `devicePk` is settled inside connect(), so a reconnect can clear it long + * after the page last rendered. A reader that computed "can I post?" from the + * field itself — chat-app.js did, through a ref — had no way to learn the + * answer had changed, and the composer stayed disabled on a connection with + * nothing whatever wrong with it and not one line in the console. + */ + set onDeviceIdentity(fn) { this._onDeviceIdentity = fn; } // Reconnecting redoes the handshake, which needs a JWT that may have gone // stale while the connection was down for minutes. Without this the // reconnect resends whatever token the original connect() call captured, @@ -631,6 +654,12 @@ class MeshBayTransport { this._newNodeBundle = null; this._newNodeBundleRecovery = null; this._joinError = null; + // Per connection, for the same reason the chat keys and the roster are + // dropped further down: the device the *previous* connection identified + // itself with is not this one's, and leaving it standing is how a + // reconnect that never got as far as announcing a device still looked, to + // the composer, exactly like one that had. + this._setDevicePk('', 'new connection'); this._pc = new RTCPeerConnection({ iceServers: await iceServers() }); this._channel = this._pc.createDataChannel('mnp', { ordered: true }); @@ -1080,16 +1109,48 @@ class MeshBayTransport { } /** + * Record — and announce — the device key this connection is identified by. + * + * Every write to `devicePk` goes through here, for two reasons: it is traced, + * so a session that ends up unable to post says so and says when; and it + * tells the page, which had no other way to find out that the answer moved. + */ + _setDevicePk(pk, why) { + const next = pk || ''; + if (next === this.devicePk) return next; + this.devicePk = next; + console.log('[MeshBay] device identity:', + next ? 'identified' : 'NOT identified — chat cannot post', + '(' + why + ')'); + trace('device_identity', { identified: !!next, why }); + if (this._onDeviceIdentity) { + try { this._onDeviceIdentity(!!next); } catch (e) { + console.error('[MeshBay] onDeviceIdentity handler threw:', e); + } + } + return next; + } + + /** * "This connection is device X of account Y", signed with the device key. * * Best effort by construction: a browser that has not recovered its identity - * keys has nothing to sign with, and a node older than MNP 1.2 does not know - * the message. Neither is an error — the node simply keeps the weaker - * attribution it had before, which is what every client did until now. + * keys has nothing to sign with. What it is *not* is silent — and it had + * three exits that were. Two early returns left whatever the previous + * connection had settled on standing; and a reply that is not + * `device_hello_ack` wiped the key without a word, because an `error` reply + * does not throw and so never reached the `.catch()` at the call site. The + * result is a chat that cannot post on a connection with nothing else wrong + * with it, which is unreadable from the outside — the shape of the "chat + * hangs, the textbox is dead" report. Every exit below names itself. */ async _announceDevice() { - if (!this._sessionKeys || !this._sessionKeys.skEdB64) return; - if (!this._nonceNode || !this.nodePk || !this._userId) return; + if (!this._sessionKeys || !this._sessionKeys.skEdB64) { + return this._setDevicePk('', 'no identity key in this session'); + } + if (!this._nonceNode || !this.nodePk || !this._userId) { + return this._setDevicePk('', 'handshake state incomplete'); + } const C = window.MeshBayCrypto; // Derived from our own secret key, never read back from anywhere — the same @@ -1106,8 +1167,11 @@ class MeshBayTransport { const resp = await this._sendAndWait({ type: 'device_hello', v: '2.0', pk_ed25519: pkEdB64, ts, sig, }); - this.devicePk = (resp && resp.type === 'device_hello_ack') ? pkEdB64 : ''; - return this.devicePk; + if (!resp || resp.type !== 'device_hello_ack') { + return this._setDevicePk('', 'node answered ' + ((resp && resp.type) || 'nothing') + + ((resp && resp.detail) ? ': ' + resp.detail : '')); + } + return this._setDevicePk(pkEdB64, 'device_hello_ack'); } /** @@ -2979,7 +3043,17 @@ class MeshBayTransport { // some field of its own — and why the ones that carry no such field // reached their caller by luck. An older node ignores the extra key and // is routed by the per-type fallbacks below, exactly as before. - this._send({ ...obj, req_id: id }); + // `_send` throws synchronously when the channel is not open. Rejecting + // on that is right, but the pending entry and its 30s timer were left + // behind — so a request that never reached the wire still logged a + // "Response timeout" half a minute later, for a reply nobody was owed. + try { + this._send({ ...obj, req_id: id }); + } catch (e) { + clearTimeout(timeout); + this._pending.delete(id); + reject(e); + } }); } |