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.js183
1 files changed, 11 insertions, 172 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 99e3fb9..2e3712c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -115,8 +115,7 @@ function _replayBroadcast(transport, msg) {
}
const ADMIN_OP_TYPES = new Set([
- 'tmdb_override', 'tmdb_rematch', 'tmdb_config', 'tmdb_enabled', 'video_root', 'audio_root',
- 'photo_roots',
+ 'tmdb_override', 'tmdb_rematch', 'tmdb_config', 'tmdb_enabled',
'musicbrainz_enabled', 'file_delete', 'dir_delete',
'apps_enabled', 'set_scan_settings', 'member_revoke',
'root_add', 'root_remove', 'root_update', 'root_eject', 'root_plug',
@@ -339,12 +338,6 @@ class Lease {
this._wait = new Promise((resolve) => { this._granted = resolve; });
}
- /** No slots on this node: behave as though one was granted at once. */
- _skip() {
- this.state = 'granted';
- this._granted();
- }
-
_request() {
// A closed channel is not a failure here, and must not throw: the transport
// reconnects on its own, `_reopenTransfers` re-asks for every live lease
@@ -417,7 +410,6 @@ class Lease {
this.closed = true;
clearTimeout(this._watchdog);
this.transport._leases.delete(this.tr);
- if (!this.transport.supportsTransferSlots) return;
try {
this.transport._send({ type: 'transfer_close', v: '0.1', tr: this.tr,
reason });
@@ -530,48 +522,9 @@ class MeshBayTransport {
/** The MNP version the connected node declared, or '' before a handshake. */
get nodeVersion() { return this._nodeVersion || ''; }
- /**
- * Whether this node hands out transfer slots.
- *
- * Read from the handshake ack rather than from the MNP version: the caps
- * shipped before the version bump that will make leases compulsory, so for
- * now a node either answers with `transfer_limits` or it predates all of
- * this. A node that does not is asked for nothing and enforces nothing —
- * every download behaves exactly as it did.
- */
- get supportsTransferSlots() { return this._transferLimits !== null; }
-
/** This member's own caps in this group, or null when the node said nothing. */
get transferLimits() { return this._transferLimits; }
- /**
- * Whether the node speaks the per-root and per-app operations MNP 1.1 added:
- * `root_update`/`root_eject`/`root_plug`, `app_directories`,
- * `chat_directory`, `chat_link_preview`.
- *
- * An older node has no equivalent for the root ones at all, and answers the
- * app ones through their three predecessors (`video_root`, `audio_root`,
- * `photo_roots`). The caller chooses which; what it must not do is send a
- * 1.1 message and wait, because an unknown type is logged and dropped.
- */
- get supportsAppOps() {
- const m = /^(\d+)\.(\d+)$/.exec(this._nodeVersion || '');
- if (!m) return false;
- return (Number(m[1]) > 1) || (Number(m[1]) === 1 && Number(m[2]) >= 1);
- }
- /**
- * Whether the node opens a sealed upload (MNP 2.0).
- *
- * A 1.x node reads `filename` and `data` off the message itself, finds
- * neither — they are inside the seal — and answers "Missing filename or
- * data", an error about the wrong thing that names no upload_id and so fails
- * every upload in flight. Asked before sending rather than discovered after,
- * for the same reason `supportsAppOps` is.
- */
- get supportsSealedUpload() {
- const m = /^(\d+)\.(\d+)$/.exec(this._nodeVersion || '');
- return !!m && Number(m[1]) >= 2;
- }
set onAppsEnabled(fn) { this._onAppsEnabled = fn; }
set onAppDirectories(fn) { this._onAppDirectories = fn; }
set onChatDirectory(fn) { this._onChatDirectory = fn; }
@@ -867,11 +820,9 @@ class MeshBayTransport {
// block, because everything below — the join, the proof, the sealed ack
// — assumes both sides mean the same thing by each message.
_checkNodeVersion(reply);
- // Kept, not just checked. Several controls exist only on a node new
- // enough to have them, and the alternative to asking is offering a
- // button whose message an older node logs as unknown and never answers
- // — a 30-second wait ending in a timeout, with nothing on screen to say
- // the node simply cannot do this.
+ // Kept for diagnostics only. Nothing branches on it: the range check
+ // above is what decides whether these two can talk at all, and a peer it
+ // admits speaks every message in this file.
this._nodeVersion = String(reply.v || '');
if (!window.MeshBayCrypto) {
throw new Error('Node requires GEK proof but no crypto available');
@@ -1075,15 +1026,6 @@ class MeshBayTransport {
Object.assign(ack, config);
this._transferLimits = ack.transfer_limits || null;
- // Tell the node which of this account's devices is on this connection.
- // Deliberately after the ack, and gated on the node's own version rather
- // than sent hopefully: a node that does not know the message answers
- // nothing at all, which would leave a `device_hello` sitting in
- // `_pending` for the full 30s — and the arrival-order fallback hands an
- // unrouted reply to the *oldest* pending request, which right after a
- // handshake is exactly this one. That is the routed-by-luck bug the chat
- // ack comment above was written for; not repeating it.
- this._nodeMnp = String(ack.v || '');
// From the *sealed* part of the ack: a forged epoch would have this
// client sealing under a key the group has retired.
this.chatEpoch = ack.chat_epoch || 0;
@@ -1093,9 +1035,10 @@ class MeshBayTransport {
this._chatKeysInFlight = null;
this._roster = null;
this._rosterInFlight = null;
- // Not gated on a version any more: a node that reached this point speaks
- // MNP 2.0, where identifying the device is what makes chat possible at
- // all. `check_version` refused anything older before we got here.
+ // Tell the node which of this account's devices is on this connection,
+ // after the ack and unconditionally: a peer `check_version` admitted
+ // speaks this message, and identifying the device is what makes chat
+ // possible at all.
await this._announceDevice().catch((e) => {
console.warn('[MeshBay] device_hello failed — chat will not work:', e);
});
@@ -1349,10 +1292,6 @@ class MeshBayTransport {
openTransfer({ kind = 'download', bytes = 0, chunks = 0, onState = null } = {}) {
const tr = _hex(crypto.getRandomValues(new Uint8Array(16)));
const lease = new Lease(this, tr, kind, bytes, chunks, onState);
- if (!this.supportsTransferSlots) {
- lease._skip();
- return lease;
- }
this._leases.set(tr, lease);
lease._request();
return lease;
@@ -1360,7 +1299,6 @@ class MeshBayTransport {
/** Re-ask for every live lease. Called after a reconnect. */
_reopenTransfers() {
- if (!this.supportsTransferSlots) return;
for (const lease of this._leases.values()) {
// The node lost the lease with the session, so this is a fresh request
// for the same `tr` — which the node treats as the same transfer rather
@@ -1450,7 +1388,7 @@ class MeshBayTransport {
}
/**
- * Correct a wrong automatic TMDB match. Signed like setVideoRoot/
+ * Correct a wrong automatic TMDB match. Signed like
* setTmdbConfig: it replaces what every member sees for a show/movie,
* node-wide (media_cache is shared, not per-viewer) — an unsigned
* override would let any member vandalize another show's metadata.
@@ -1520,7 +1458,7 @@ class MeshBayTransport {
* Whether TMDB lookups run for this group at all — per-group (2026-08-24,
* used to be node-wide): a real media-library group and a test/demo group
* on the same node need not share the decision to spend TMDB quota and
- * make outbound requests. Signed like setVideoRoot — it decides whether
+ * make outbound requests. Signed like the rest — it decides whether
* this group's members' Videos tab ever makes outbound TMDB traffic.
*/
async setTmdbEnabled(enabled, signFn) {
@@ -1538,100 +1476,6 @@ class MeshBayTransport {
return msg;
}
- /**
- * Which folder (possibly a subfolder of a shared root) the Videos app
- * treats as its entry point for this group. `path: ''` means the whole
- * group index. Signed like setAppsEnabled — it decides what every
- * member's Videos tab shows.
- */
- async setVideoRoot(path, signFn) {
- const clean = (path || '').replace(/^\/+|\/+$/g, '');
- const msg = await this._sendAndWait({ type: 'video_root', v: '0.5', path: clean });
- if (msg.type === 'error') throw new Error(msg.detail);
- if (msg.type === 'admin_challenge') {
- return this._authorizeAdminOp(msg, 'video_root', clean, signFn);
- }
- return msg;
- }
-
- /**
- * Same shape as setVideoRoot above — the Music app's own entry point.
- */
- async setAudioRoot(path, signFn) {
- const clean = (path || '').replace(/^\/+|\/+$/g, '');
- console.log('[MeshBay] setAudioRoot: sending request, path=', JSON.stringify(clean));
- const msg = await this._sendAndWait({ type: 'audio_root', v: '0.10', path: clean });
- console.log('[MeshBay] setAudioRoot: first reply =', msg);
- if (msg.type === 'error') throw new Error(msg.detail);
- if (msg.type === 'admin_challenge') {
- return this._authorizeAdminOp(msg, 'audio_root', clean, signFn);
- }
- return msg;
- }
-
- /**
- * Which folder(s) the Photos app treats as its entry points for this
- * group (docs/photos.md §2.1). Unlike setVideoRoot/setAudioRoot, `roots`
- * is a whole set, replaced in one signed op — same shape as
- * setAppsEnabled. The client normalizes the same way the node does
- * (webrtc_server.py's `_do_photo_roots`: trim slashes, drop empties,
- * dedupe, sort) so the subject built here matches byte-for-byte what the
- * node signs the challenge against.
- */
- async setPhotoRoots(roots, signFn) {
- const clean = [...new Set(
- (roots || []).map((r) => (r || '').replace(/^\/+|\/+$/g, '')).filter(Boolean),
- )].sort();
- const msg = await this._sendAndWait({ type: 'photo_roots', v: '0.11', roots: clean });
- if (msg.type === 'error') throw new Error(msg.detail);
- if (msg.type === 'admin_challenge') {
- return this._authorizeAdminOp(msg, 'photo_roots', clean.join(','), signFn);
- }
- return msg;
- }
-
- /**
- * Point an application at folder(s) inside the group's shared directories.
- *
- * One method for every app, keyed by the app's registry name — the same
- * generic op the node grew for the same reason (docs/refactor-groups.md
- * §1.6). `setVideoRoot`, `setAudioRoot` and `setPhotoRoots` are still here
- * and still work; nothing new should call them.
- *
- * The subject names the app as well as the paths, because an operator shown
- * "Media/Films" alone cannot tell which application is about to be pointed
- * at it, and two apps' challenges would otherwise be indistinguishable.
- * Cleaned and sorted the same way the node does, so both sides build the
- * same bytes to sign.
- */
- /**
- * The same instruction a node too old for `app_directories` understands.
- *
- * Videos, Music and Photos each had their own message before this, and they
- * still work — so an operator on an un-upgraded node keeps the ability they
- * had, rather than being handed a control that silently times out. Chat has
- * no predecessor, which is why its settings are hidden rather than routed.
- */
- async setAppDirectoriesLegacy(appKey, directories, signFn) {
- const clean = [...new Set(
- (directories || []).map((d) => (d || '').replace(/^\/+|\/+$/g, '')).filter(Boolean),
- )].sort();
- if (appKey === 'photo') return this.setPhotoRoots(clean, signFn);
- // One folder was all these two could carry. Sending several would store
- // the first and silently drop the rest, so it is refused instead.
- if (clean.length > 1) {
- throw new Error(
- 'This node is older than this page and can hold one folder per app. '
- + 'Update it, or choose a single folder.');
- }
- const one = clean[0] || '';
- if (appKey === 'video') return this.setVideoRoot(one, signFn);
- if (appKey === 'music') return this.setAudioRoot(one, signFn);
- throw new Error(
- 'This node is older than this page and cannot store this app\'s '
- + 'folders. Its operator has to update it.');
- }
-
async setAppDirectories(appKey, directories, signFn) {
const clean = [...new Set(
(directories || []).map((d) => (d || '').replace(/^\/+|\/+$/g, '')).filter(Boolean),
@@ -2471,11 +2315,6 @@ class MeshBayTransport {
throw new Error(`${file.name} is already being uploaded`);
}
if (!this._gekRaw) throw new Error('This group has no key on this device');
- if (!this.supportsSealedUpload) {
- throw new Error(
- 'This node is running an older MeshBay and cannot accept an upload '
- + 'from this page. Its operator has to update it.');
- }
const C = window.MeshBayCrypto;
const groupId = (this._connectArgs && this._connectArgs.groupId) || '';
this._inFlightUploads.add(file.name);
@@ -3143,7 +2982,7 @@ class MeshBayTransport {
// generic "oldest pending" fallback further down. Returns as soon as a
// match resolves: this transport instance is the one that submitted
// the request, and its own caller already updates local state from
- // what *it* sent (setAppsEnabled/setVideoRoot/... callers all do
+ // what *it* sent (setAppsEnabled/setAppDirectories/... callers all do
// `onX(next)` with their own local value, never by reading the ack),
// so the broadcast-oriented per-type handlers below — there for every
// *other* connected client learning the change — have nothing left to