summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-20 08:56:23 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-20 08:56:23 +0200
commitc8af746c846b5dbc792f7e4f0d806647d513cc5c (patch)
tree56c54ec5c703ec042589697b22b53e42451a61e4 /packages/meshbay-hub/src/meshbay_hub/static/transport.js
parent90c3c6a01f46c9b29c5d92702d7383f32e8951d7 (diff)
downloadmeshbay-c8af746c846b5dbc792f7e4f0d806647d513cc5c.tar.gz
feat(node): full Node admin panel — CLI parity, hot-reload, group lifecycle
Node admin panel (NodePage) now covers every CLI operation over MNP: group attach/detach, roster, member unpin, GEK rotate, denylist, reload. Daemon hot-loads new groups and tears down removed ones on config reload instead of requiring a full restart. Group attach/detach via MNP or local API triggers an automatic reload so the group is live immediately. Fixed GroupPage hang on first visit to a newly created group: the JWT issued at login didn't include the new group, the node rejected with not_a_member, and the token-refresh path returned without re-triggering the connect effect (Boolean(token) didn't change). Now bumps retryKey after a successful refresh so the effect re-runs with the fresh token. NodePage marks groups hosted by the node but absent from the hub with a "not on hub" badge so stale groups are visible and easy to remove. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js120
1 files changed, 117 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index a0509a0..17886f4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -217,6 +217,9 @@ class MeshBayTransport {
await this._pc.setRemoteDescription({ type: 'answer', sdp: answer.sdp });
await channelReady;
+ console.log('[MeshBay] DataChannel open, sending handshake for group', groupId,
+ 'channel=', this._channel?.readyState,
+ 'crypto=', !!window.MeshBayCrypto);
// The client nonce is what makes the NODE's proof fresh (C3) — without it a
// recorded handshake_ack could be replayed by an impersonating peer.
@@ -229,6 +232,7 @@ class MeshBayTransport {
group_id: groupId || '',
nonce: window.MeshBayCrypto.b64encode(this._nonceClient),
});
+ console.log('[MeshBay] Handshake reply:', reply.type);
if (reply.type === 'handshake_challenge') {
if (!window.MeshBayCrypto) {
@@ -380,11 +384,9 @@ class MeshBayTransport {
// A node that answers a handshake with anything other than a challenge is not
// running the mutual protocol. Accepting a bare handshake_ack here would let a
// peer skip proving GEK possession entirely (C3/C6).
+ console.warn('[MeshBay] Handshake rejected:', reply.detail, 'code:', reply.code);
const rejected = new Error(
'MNP handshake rejected: ' + (reply.detail || `unexpected ${reply.type}`));
- // `not_a_member` usually means our token predates being added to the group;
- // the caller refreshes it and tries again rather than showing that to someone
- // who was invited thirty seconds ago.
rejected.reason = reply.code || '';
throw rejected;
}
@@ -622,6 +624,112 @@ class MeshBayTransport {
return msg;
}
+ // ── Node management (D5) ───────────────────────────────────────────────
+
+ async fetchNodeStatus() {
+ const msg = await this._sendAndWait({ type: 'node_status', v: '0.1' });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
+ async addRoot(groupId, path, { name, kind, upload } = {}, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'root_add', v: '0.1',
+ group_id: groupId, path,
+ name: name || '', kind: kind || 'generic', upload: !!upload,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'root_add', path, signFn);
+ }
+ return msg;
+ }
+
+ async removeRoot(groupId, rootName, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'root_remove', v: '0.1',
+ group_id: groupId, root_name: rootName,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'root_remove', rootName, signFn);
+ }
+ return msg;
+ }
+
+ async unpinMember(userId, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'member_unpin', v: '0.1', user_id: userId,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'member_unpin', userId, signFn);
+ }
+ return msg;
+ }
+
+ async rotateGek(groupId, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'gek_rotate', v: '0.1', group_id: groupId,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'gek_rotate', groupId, signFn);
+ }
+ return msg;
+ }
+
+ async fetchRoster(groupId) {
+ const msg = await this._sendAndWait({
+ type: 'roster_read', v: '0.1', group_id: groupId || '',
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
+ async fetchDenylist() {
+ const msg = await this._sendAndWait({ type: 'denylist_read', v: '0.1' });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
+ async clearDenylist(subject) {
+ const msg = await this._sendAndWait({
+ type: 'denylist_clear', v: '0.1', subject: subject || '',
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
+ async attachGroup(name, sharedDir, uploadDir, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'group_attach', v: '0.1',
+ name, shared_dir: sharedDir, upload_dir: uploadDir || '',
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'group_attach', name, signFn);
+ }
+ return msg;
+ }
+
+ async detachGroup(name, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'group_detach', v: '0.1', name,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'group_detach', name, signFn);
+ }
+ return msg;
+ }
+
+ async reloadConfig() {
+ const msg = await this._sendAndWait({ type: 'node_reload', v: '0.1' });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
/**
* Ask for a video stream, and say how much we can take.
*
@@ -1003,6 +1111,8 @@ class MeshBayTransport {
const id = this._seqId++;
const timeout = setTimeout(() => {
this._pending.delete(id);
+ console.error('[MeshBay] Response timeout for', obj.type,
+ 'after', timeoutMs, 'ms, channel=', this._channel?.readyState);
reject(new Error('Response timeout'));
}, timeoutMs);
this._pending.set(id, {
@@ -1040,6 +1150,10 @@ class MeshBayTransport {
_onMessage(data) {
const incoming = new Uint8Array(data);
+ this._msgCount = (this._msgCount || 0) + 1;
+ if (this._msgCount <= 3) {
+ console.log('[MeshBay] recv', incoming.length, 'bytes, msg #' + this._msgCount);
+ }
const combined = new Uint8Array(this._recvBuf.length + incoming.length);
combined.set(this._recvBuf);
combined.set(incoming, this._recvBuf.length);