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.js88
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);
}