diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-23 18:05:14 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-23 18:05:14 +0200 |
| commit | 35a7764db3f58a93c32206cb3ce74bb2f03967e7 (patch) | |
| tree | d2acb2a07ee6dc2f4241fcc785c2860d57263892 /packages/meshbay-hub/src/meshbay_hub/static/group-page.js | |
| parent | 998f9c69308ee88fac36cfb77dfb6d07c6fa926a (diff) | |
| download | meshbay-35a7764db3f58a93c32206cb3ce74bb2f03967e7.tar.gz | |
feat(hub): open, create and join invitation links in the interface
#/invite takes the link out of the address on load and keeps it in the
tab through registration and sign-in; joining is one click, only the
ticket goes to the hub, and the code goes only to the node the link
names once it has signed its challenge. Members tab gains "Invite by
link" (shared e-mail box, pending list, cancel both halves); home page
takes a pasted link. Browser probe drives the real app, signed out and in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-page.js | 31 |
1 files changed, 28 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 4fdf5f8..a25c07b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -16,6 +16,7 @@ import { FilePreview } from './files-app.js'; import { VideoPlayer } from './video-player.js'; import { GroupSettingsPanel } from './group-settings.js'; import { reportIndexPush } from './index-dock.js'; +import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js'; /** * The group shell: everything a group's "applications" (Chat, Files, and @@ -398,6 +399,17 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // `transport` holds the attempt in progress, and keeps whichever one // answers — so it is null after the loop exactly when none did. let transport = null, ack = null, lastErr = null; + // An invitation link for this group names the node that holds its + // code: that node is tried first, and only it is handed the code — + // the transport refuses any other (docs/MESHBAY_DESIGN.md §3.4). A + // code typed into the form takes precedence and goes as it always has. + const link = session.pendingJoinCode ? null : pendingFor(groupId); + const joinCode = session.pendingJoinCode || (link ? link.c : null); + const joinNodePk = link ? nodePkFromLink(link.n) : undefined; + if (link) { + nodesData.nodes.sort((a, b) => + (b.pk_node === joinNodePk) - (a.pk_node === joinNodePk)); + } for (const n of nodesData.nodes) { // The same base the API calls use: signaling is a hub endpoint like // any other, and two sources for one address is how they drift. @@ -417,7 +429,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, try { ack = await transport.connect( n.node_id, live, groupId, null, sessionKeys, session.bundleKey, - username, userId, session.pendingJoinCode, session.recoveryKey); + username, userId, joinCode, session.recoveryKey, joinNodePk); break; } catch (e) { lastErr = e; @@ -432,12 +444,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // A refusal that names a state of *this browser* — a code to enter, // a passphrase, a device to approve — is the same answer from every // node, and the operator to act on is this one's. Trying the next - // node would only replace it with a less useful message. - if (e.reason && e.reason !== 'not_hosted') throw e; + // node would only replace it with a less useful message. The one + // exception besides `not_hosted` is a link naming another host. + if (e.reason && e.reason !== 'not_hosted' && e.reason !== 'link_other_node') { + throw e; + } } } if (!transport) throw (lastErr || new Error('no node served this group')); session.pendingJoinCode = null; + // In: the invitation has done its job, whether its code was spent now or + // the node already knew us. + if (link) clearPending(); if (cancelled) return; applyAck(ack); transport.onAppsEnabled = (apps) => setEnabledApps(apps); @@ -617,6 +635,13 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // one-time code from the operator before it will hand over the group // key. Not an error to shout about — a step in joining. if (err.reason === 'code_required') setNeedsCode(true); + // The link's code was refused by the node that issued it — used, or + // cancelled. It will not work on another try; say so, and drop it. + if (err.reason === 'code_invalid' && pendingFor(groupId) + && !session.pendingJoinCode) { + clearPending(); + err.message = t('group.link_spent'); + } // The node has no bundle for us and this browser derived no key to make // one — the passphrase form below is the way in, not a support request. if (err.reason === 'no_keys') setNeedsPass(true); |