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 | 70 |
1 files changed, 66 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index c43422f..f6adaac 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -123,7 +123,7 @@ const ADMIN_OP_TYPES = new Set([ 'root_add', 'root_remove', 'root_update', 'root_eject', 'root_plug', 'app_directories', 'chat_directory', 'chat_link_preview', 'chat_epoch', 'search_listed', 'member_unpin', 'gek_rotate', 'group_attach', - 'group_detach', 'invite_create', + 'group_detach', 'invite_create', 'invite_link_create', 'invite_cancel', ]); // ── Diagnostic trace (opt-in, off by default) ─────────────────────────────── @@ -614,13 +614,14 @@ class MeshBayTransport { } async connect(nodeId, jwtToken, groupId, gekRaw, sessionKeys, bundleKey, username, - userId, joinCode, recoveryKey) { + userId, joinCode, recoveryKey, joinNodePk) { // Remembered for _reconnectLoop, which calls connect() again with these // same values (plus a freshly-fetched token and the identity connect() // itself settles on below) after the WebRTC connection is declared // "failed" — see the pc.onconnectionstatechange handler further down. this._connectArgs = { nodeId, groupId, gekRaw, bundleKey, username, userId, joinCode, recoveryKey, + joinNodePk, }; this._lastToken = jwtToken; // The constructor sets this once from whatever token the caller had at @@ -984,7 +985,16 @@ class MeshBayTransport { // This is the normal path for anyone who joined after the invite redesign — // no bundle is pre-stored for members any more. A code is needed only the // first time this node sees this account. - if (!gekRaw && this._sessionKeys && userId) { + // A code from an invitation link goes to the node the link names and to + // no other, and only once that node has proved the key in its challenge + // (docs/MESHBAY_DESIGN.md §3.4). Otherwise nothing is sent at all — not + // even a join without the code, which this node would answer by asking + // for one. + const linkRefusal = _linkJoinRefusal(joinNodePk, joinCode, this.nodePk, + this.nodePkProved); + if (linkRefusal) { + this._joinError = linkRefusal; + } else if (!gekRaw && this._sessionKeys && userId) { try { gekRaw = await this.joinGroup(userId, groupId, joinCode); } catch (e) { @@ -1240,7 +1250,8 @@ class MeshBayTransport { try { ack = await this.connect(args.nodeId, token, args.groupId, args.gekRaw, this._sessionKeys, args.bundleKey, args.username, - args.userId, args.joinCode); + args.userId, args.joinCode, undefined, + args.joinNodePk); } finally { this._inReconnectAttempt = false; } @@ -2627,6 +2638,34 @@ class MeshBayTransport { } /** + * A code bound to no account, for an invitation link (MNP 3.4). Signed like + * any invitation, and the subject the operator signs is the outcome: a link + * into this group, `link:<group>`, and nothing else. + */ + async createLinkInvite(groupId, signFn) { + const msg = await this._sendAndWait({ + type: 'invite_link_create', v: '0.1', group_id: groupId, + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + return this._authorizeAdminOp(msg, 'invite_link_create', `link:${groupId}`, signFn); + } + return msg; + } + + /** Take back an unredeemed invitation link, by the handle it was issued with. */ + async cancelLinkInvite(inviteId, signFn) { + const msg = await this._sendAndWait({ + type: 'invite_cancel', v: '0.1', invite_id: inviteId, + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + return this._authorizeAdminOp(msg, 'invite_cancel', inviteId, signFn); + } + return msg; + } + + /** * Ask the node to recognise us and hand over the group key. * * Sent when we hold no GEK for a group. `code` is needed only the first time @@ -3982,6 +4021,29 @@ function _hex(bytes) { } /** + * Why a code from an invitation link must not go to this node, or null. + * + * `link_other_node` is the caller's cue to try the next node the hub listed, + * as for `not_hosted`: the link names one node, and this is not it. An older + * node that cannot prove its key early is refused rather than trusted — it + * cannot have issued a link code anyway. + */ +function _linkJoinRefusal(joinNodePk, joinCode, nodePk, nodePkProved) { + if (!joinNodePk || !joinCode) return null; + if (nodePk !== joinNodePk) { + const err = new Error('This invitation was issued by another machine hosting this group.'); + err.reason = 'link_other_node'; + return err; + } + if (!nodePkProved) { + const err = new Error('This node is too old to accept invitation links.'); + err.reason = 'link_node_unproved'; + return err; + } + return null; +} + +/** * Whether `handshake_challenge` proves the key it announces (MNP 3.4). * * True when it carries a signature that verifies over this connection, false |