aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js70
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