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 | 88 |
1 files changed, 56 insertions, 32 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 482574f..8df7700 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -72,11 +72,18 @@ function _aborted() { // change anything — `op` is already on every admin_challenge, and this // list is what lets a response two steps later be tied back to the right // one. +// The acks whose payload is state no caller could have predicted: they carry +// the node's whole roots table back. See the note where they are dispatched. +const ROOT_ACK_TYPES = new Set([ + 'root_update_ack', 'root_eject_ack', 'root_plug_ack', + 'root_add_ack', 'root_remove_ack', +]); + const ADMIN_OP_TYPES = new Set([ 'tmdb_override', 'tmdb_rematch', 'tmdb_config', 'tmdb_enabled', 'video_root', 'audio_root', 'photo_roots', 'musicbrainz_enabled', 'file_delete', 'dir_delete', - 'member_upload', 'apps_enabled', 'set_scan_settings', 'member_revoke', + 'apps_enabled', 'set_scan_settings', 'member_revoke', 'root_add', 'root_remove', 'root_update', 'root_eject', 'root_plug', 'member_unpin', 'gek_rotate', 'group_attach', 'group_detach', 'invite_create', @@ -1072,7 +1079,7 @@ class MeshBayTransport { * queried in (e.g. "fr-FR") — one for the whole node, since both are one * operator's shared credential/cache, not a per-group concern (see * setTmdbEnabled below for the per-group on/off switch). Signed like - * setAppsEnabled/setMemberUpload — an unsigned change would let any + * setAppsEnabled/updateRoot — an unsigned change would let any * member alter outbound third-party network traffic the operator never * agreed to (docs/mediacenter.md §5.5, §8). `token: ''` explicitly clears * a previously-set custom token; omit it (undefined/null), like @@ -1367,24 +1374,6 @@ class MeshBayTransport { * on the hub is the other half, and neither implies the other. */ /** - * Turn uploading by ordinary members on or off. - * - * Signed by the operator like any other privileged operation — the node - * refuses an unsigned one, which is what stops a member turning it back on. - */ - async setMemberUpload(allowed, signFn) { - const msg = await this._sendAndWait({ - type: 'member_upload', v: '0.1', allowed: Boolean(allowed), - }); - if (msg.type === 'error') throw new Error(msg.detail); - if (msg.type === 'admin_challenge') { - return this._authorizeAdminOp( - msg, 'member_upload', allowed ? 'on' : 'off', signFn); - } - return msg; - } - - /** * Turn a group "application" (Chat, Files, ...) on or off for everyone. * * Takes the whole set in one signed message rather than one op per app, so @@ -1393,13 +1382,25 @@ class MeshBayTransport { * `_authorizeAdminOp` below checks the two match. */ async setAppsEnabled(apps, signFn) { + // Files cannot be turned off — MNP permits root exploration regardless of + // this list, so hiding the tab only ever misled — and the node adds it if + // it is missing. That normalisation has to happen *here too*: the subject + // below is rebuilt from what this client sent, and compared byte for byte + // against what the node put in the challenge. A list arriving here without + // `files` would produce two different strings and `_authorizeAdminOp` + // would refuse to sign an op the operator did ask for. It is reachable + // only from a caller that builds the list from something other than the + // node's own answer, which is exactly the kind of caller a later phase + // adds. (`apps.js` marks it `alwaysEnabled`; this file is a classic + // script and cannot import it.) + const full = apps.includes('files') ? [...apps] : ['files', ...apps]; const msg = await this._sendAndWait({ - type: 'apps_enabled', v: '0.1', apps, + type: 'apps_enabled', v: '0.1', apps: full, }); if (msg.type === 'error') throw new Error(msg.detail); if (msg.type === 'admin_challenge') { return this._authorizeAdminOp( - msg, 'apps_enabled', [...apps].sort().join(','), signFn); + msg, 'apps_enabled', [...full].sort().join(','), signFn); } return msg; } @@ -1657,8 +1658,14 @@ class MeshBayTransport { * The node decides where this lands (uploads/) and under what name — it finds a * free one rather than replacing anything. The ack says which, and that is what * this returns. + * + * `root` names which shared directory to upload into — a name, never a path; + * the node picks the destination inside it. Since a group can have several + * writable roots, leaving it out is a guess, and the node's fallback ("the + * first writable one") exists only for MNP 1.0 clients, which had exactly one + * destination. Every caller here browses a root and knows which one it is. */ - async uploadFile(file, { chunkSize, onProgress, signal } = {}) { + async uploadFile(file, { chunkSize, onProgress, signal, root } = {}) { // The same file twice at once would confuse the node, which keys its own // upload state by name — and would race for the same destination. if (this._uploaders.has(file.name)) { @@ -1709,6 +1716,7 @@ class MeshBayTransport { chunk_index: i, total_chunks: total, data: buf, + ...(root ? { root } : {}), }); } while (acked < total) { @@ -2206,7 +2214,21 @@ class MeshBayTransport { } else if (typeof msg.type === 'string' && msg.type.endsWith('_ack')) { const key = `admin:${msg.type.slice(0, -4)}`; for (const [, handler] of this._pending) { - if (handler._key === key) { handler.resolve(msg); return; } + if (handler._key === key) { + handler.resolve(msg); + // The comment above ("its own caller already updates local state + // from what it sent") is true of every op whose caller passes the + // value it just chose to an onX(next). The root ops are not like + // that: what changes is the whole roots table, which only the node + // can compute — availability, the eject that the plug refused, the + // name it settled on. Returning here left the operator who clicked + // Eject as the one client that never saw it happen, while every + // other peer got the broadcast. So this one type is handed on. + if (ROOT_ACK_TYPES.has(msg.type) && this._onRootsChanged) { + this._onRootsChanged(msg); + } + return; + } } } @@ -2253,10 +2275,12 @@ class MeshBayTransport { return; } - // The operator changed who may upload. Unsolicited: it arrives at everyone - // connected, not only at whoever asked. It still has to reach a pending - // caller — the operator's own request resolves on this reply — so it falls - // through to the matching below rather than returning here. + // Legacy. An MNP 1.0 node still broadcasts this when its operator changes + // the group-wide upload switch, and its roots carry no `writable` for us + // to read instead — so this is the only answer available from such a node + // and it is still honoured. Nothing here *sends* the message any more: + // per-root RO/RW replaced it, and a current node answers it with a + // deprecation notice and no action. if (msg.type === 'member_upload_ack' && this._onUploadPolicy) { this._onUploadPolicy(Boolean(msg.allowed)); } @@ -2322,10 +2346,10 @@ class MeshBayTransport { this._onMusicbrainzEnabled(Boolean(msg.enabled)); } - // A root's writable/removable flags changed, or a root was ejected/plugged. - // Broadcast to all peers so everyone sees the change. - if ((msg.type === 'root_update_ack' || msg.type === 'root_eject_ack' - || msg.type === 'root_plug_ack') && this._onRootsChanged) { + // A root's flags changed, or one was ejected, plugged, added or removed. + // Broadcast by the node to every peer, so everyone's table updates without + // waiting for the next index_sync. + if (ROOT_ACK_TYPES.has(msg.type) && this._onRootsChanged) { this._onRootsChanged(msg); } |