summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-10 17:29:50 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-10 17:29:50 +0200
commit4753c67816c774323e3ab4efc76d3259e8ded40d (patch)
tree0ac0b8a3013aa483111ea249e3aa37ff49988891 /packages/meshbay-hub
parent1dcedc77083b908b7b3b431bad679a4813884355 (diff)
downloadmeshbay-4753c67816c774323e3ab4efc76d3259e8ded40d.tar.gz
refactor(spa): stop asking a node what version it is
`MNP_MIN_SUPPORTED` is the version this build speaks, so `check_version` refuses everything below it at the handshake. Every capability the client was gating on the node's version is therefore true of every peer it can reach: * `supportsSealedUpload` — an upload is sealed or it is not sent; * `supportsAppOps` — one `app_directories` op, and no `setVideoRoot` / `setAudioRoot` / `setPhotoRoots` wrappers behind it; * `supportsTransferSlots` and `Lease._skip()` — a lease is always real, so there is no branch where a transfer runs without one; * `legacyNode`, the read-only shared-directories table, and the two hints telling an operator their node is too old to configure an app. The version the node declares is still recorded, for diagnostics. Nothing branches on it, and the comment says so, because a field kept "just in case" is how the branches came back last time. `test_mnp_1_0_node_compat.py` goes with them: it existed to hold the fallbacks in place, and holding a fallback that cannot execute is how a suite starts lying. The two locale strings for those hints are removed from all ten catalogues. Hub suite 872 passed (test_sticky_header deselected — failing before this). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
Diffstat (limited to 'packages/meshbay-hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js28
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js183
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-app.js2
-rw-r--r--packages/meshbay-hub/tests/harness/upload_seal_probe.mjs1
-rw-r--r--packages/meshbay-hub/tests/test_app_settings_plugin.py6
-rw-r--r--packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py144
-rw-r--r--packages/meshbay-hub/tests/test_transport_contracts.py7
-rw-r--r--packages/meshbay-hub/tests/test_upload_seal_client.py15
19 files changed, 26 insertions, 386 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 0f442cf..341d37e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -145,10 +145,6 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// destination rather than a set of folders it reads.
const [chatDirectory, setChatDirectory] = useState('');
const [chatLinkPreview, setChatLinkPreview] = useState(true);
- // Whether this node speaks the operations MNP 1.1 added. False for one that
- // predates them, and the Settings page then offers what that node can
- // actually do rather than controls whose messages it drops unanswered.
- const [nodeSupportsAppOps, setNodeSupportsAppOps] = useState(false);
// MusicBrainz on/off (per-group) — docs/musicbay.md §3.2.
const [musicbrainzConfig, setMusicbrainzConfig] = useState(null);
const onPlayQueue = useCallback((tracks, startIndex) => {
@@ -365,7 +361,6 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
|| (ack.audio_root ? [ack.audio_root] : []),
photo: ack.photo_directories || ack.photo_roots || [],
});
- setNodeSupportsAppOps(transport.supportsAppOps);
setChatDirectory(ack.chat_directory || '');
setChatLinkPreview(ack.chat_link_preview !== false);
setMusicbrainzConfig({
@@ -827,7 +822,6 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
onScanSettings=${(s) => setScanSettings(s)}
entries=${entries} nodeDirs=${nodeDirs}
appSettings=${appSettings}
- nodeSupportsAppOps=${nodeSupportsAppOps}
${/* The saving pane already knows what it asked for; this is so
the page's own copy moves at the same time, rather than
waiting for the ack it will not be handed (transport.js
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index dce5833..8e11088 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -40,7 +40,6 @@ import * as platform from './platform.js';
* transport — MeshBayTransport instance, or null when not connected
* signFn — signing function for admin ops
* nodeDetected — whether the loopback node API answers
- * readOnly — suppress every edit control
* onRootsChange — called after a change, to re-read the loopback list
* onRefreshIndex — full index refresh. Not called after a root change: see
* `run()` for why the node's own push is what settles it
@@ -48,7 +47,7 @@ import * as platform from './platform.js';
* localRoots / onLocalRootsChange — the array, in "local" mode
*/
function SharedDirectoriesTable({ roots, groupId, transport, signFn,
- nodeDetected: nodeAvail, readOnly,
+ nodeDetected: nodeAvail,
onRootsChange, onRefreshIndex,
mode = 'live',
localRoots, onLocalRootsChange }) {
@@ -116,7 +115,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
// API is not (it is authorized by being on localhost with the run token).
const overMnp = !isLocal && transport && transport.connected;
const overLoopback = !isLocal && !overMnp && nodeAvail;
- const canEdit = !readOnly && (isLocal || overMnp || overLoopback);
+ const canEdit = isLocal || overMnp || overLoopback;
const rootUrl = (name, suffix = '') =>
'/api/groups/' + groupId + '/roots/' + encodeURIComponent(name) + suffix;
@@ -401,7 +400,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
enabledApps, onEnabledApps,
scanSettings, onScanSettings,
entries, nodeDirs,
- appSettings, nodeSupportsAppOps,
+ appSettings,
onAppDirectories, onRefreshIndex,
onPaired, onLeft }) {
const [members, setMembers] = useState([]);
@@ -693,14 +692,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
if (!transport || !transport.connected) {
throw new Error(t('node.root_no_route'));
}
- // A node too old for the generic op still answers the three per-app
- // messages that came before it, so an operator on one keeps the ability
- // they had rather than being handed a control that times out.
- if (transport.supportsAppOps) {
- await transport.setAppDirectories(appKey, paths, adminSignFn);
- } else {
- await transport.setAppDirectoriesLegacy(appKey, paths, adminSignFn);
- }
+ await transport.setAppDirectories(appKey, paths, adminSignFn);
if (onAppDirectories) onAppDirectories(appKey, paths);
}, [transportRef, adminSignFn, onAppDirectories]);
@@ -898,19 +890,11 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
<p class="settings-hint">${t('settings_node.shared_directories_hint')}</p>
${!connected && nodeDetected && html`
<p class="settings-hint">${t('settings_node.roots_offline_hint')}</p>`}
- ${/* Read-only against a node that predates the root operations:
- writable, removable, eject and plug have no older equivalent
- to fall back to, and an unknown message type is dropped
- unanswered — a thirty-second wait ending in a timeout, with
- nothing on screen to say the node simply cannot do it. */''}
- ${connected && !nodeSupportsAppOps && !nodeDetected && html`
- <p class="settings-hint">${t('settings_node.roots_node_too_old')}</p>`}
<${SharedDirectoriesTable}
roots=${effectiveRoots}
groupId=${groupId}
transport=${transportRef.current}
signFn=${adminSignFn}
- readOnly=${connected && !nodeSupportsAppOps && !nodeDetected}
nodeDetected=${nodeDetected}
onRootsChange=${loadNodeInfo}
onRefreshIndex=${onRefreshIndex} />
@@ -937,9 +921,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
disabled=${appsBusy}
onChange=${() => toggleApp(app.key)} />
`}>
- ${!nodeSupportsAppOps && app.key === 'chat'
- ? html`<p class="settings-hint">${t('settings_node.app_node_too_old')}</p>`
- : activeApps.includes(app.key)
+ ${activeApps.includes(app.key)
? html`<${app.Settings}
roots=${effectiveRoots} dirs=${folderOptions}
settings=${appSettings}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 0f632e3..9d4aef8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -896,8 +896,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Registrieren Sie sich bei TMDB, um einen eigenen API-Schlüssel zu erzeugen.',
'settings_app.tmdb_token_link': 'Schlüssel holen',
'settings_node.roots_offline_hint': 'Nicht mit dem Node verbunden — Änderungen laufen über den lokalen Node und greifen beim nächsten Neuladen.',
- 'settings_node.roots_node_too_old': 'Dieser Node ist älter als diese Seite: Er kann seine Verzeichnisse anzeigen, aber hier nicht ändern. Aktualisieren Sie ihn oder nutzen Sie die meshbay-node root-Befehle.',
- 'settings_node.app_node_too_old': 'Dieser Node ist älter als diese Seite und hat für diese App noch keine Einstellung. Aktualisieren Sie ihn, um sie hier zu konfigurieren.',
'settings_node.directories_title': 'App-Verzeichnisse',
'settings_node.directories_hint': 'Freigegebene Ordner und welchen davon die Videos-, Musik- und Fotos-Apps als eigene(n) Einstiegspunkt(e) nutzen.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index cb7f4a0..8385caf 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -650,8 +650,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Sign up on TMDB to generate your own API key.',
'settings_app.tmdb_token_link': 'Get a key',
'settings_node.roots_offline_hint': 'Not connected to the node — changes go through the local node instead, and take effect on its next reload.',
- 'settings_node.roots_node_too_old': 'This node is older than this page: it can show its directories but not change them here. Update it, or use the meshbay-node root commands.',
- 'settings_node.app_node_too_old': 'This node is older than this page and has no setting for this app yet. Update it to configure this here.',
'settings_node.directories_title': 'App directories',
'settings_node.directories_hint': 'Which shared folders the Videos, Music and Photos apps use as their entry point(s).',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index fe1c13b..cb8f312 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -891,8 +891,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Regístrate en TMDB para generar tu propia clave de API.',
'settings_app.tmdb_token_link': 'Obtener una clave',
'settings_node.roots_offline_hint': 'Sin conexión con el nodo: los cambios pasan por el nodo local y se aplican en su próxima recarga.',
- 'settings_node.roots_node_too_old': 'Este nodo es más antiguo que esta página: puede mostrar sus directorios pero no cambiarlos aquí. Actualízalo o usa los comandos meshbay-node root.',
- 'settings_node.app_node_too_old': 'Este nodo es más antiguo que esta página y aún no tiene ajustes para esta aplicación. Actualízalo para configurarla aquí.',
'settings_node.directories_title': 'Directorios de apps',
'settings_node.directories_hint': 'Carpetas compartidas, y cuál de ellas usan las apps de Vídeos, Música y Fotos como su(s) propio(s) punto(s) de entrada.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index ea3f60e..608da62 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -909,8 +909,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Créez un compte TMDB pour générer votre propre clé d\'API.',
'settings_app.tmdb_token_link': 'Obtenir une clé',
'settings_node.roots_offline_hint': 'Non connecté au nœud — les changements passent par le nœud local et prennent effet à son prochain rechargement.',
- 'settings_node.roots_node_too_old': 'Ce nœud est plus ancien que cette page : il peut afficher ses répertoires mais pas les modifier ici. Mettez-le à jour, ou utilisez les commandes meshbay-node root.',
- 'settings_node.app_node_too_old': 'Ce nœud est plus ancien que cette page et n\'a pas encore de réglage pour cette application. Mettez-le à jour pour la configurer ici.',
'settings_node.directories_title': 'Répertoires des applications',
'settings_node.directories_hint': 'Quel(s) dossier(s) partagés les applications Vidéos, Musique et Photos utilisent comme leur(s) propre(s) point(s) d\'entrée.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 0687b25..be5fe29 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -905,8 +905,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Registrati su TMDB per generare la tua chiave API.',
'settings_app.tmdb_token_link': 'Ottieni una chiave',
'settings_node.roots_offline_hint': 'Non connesso al nodo: le modifiche passano dal nodo locale e hanno effetto al successivo ricaricamento.',
- 'settings_node.roots_node_too_old': 'Questo nodo è più vecchio di questa pagina: può mostrare le sue directory ma non modificarle qui. Aggiornalo, oppure usa i comandi meshbay-node root.',
- 'settings_node.app_node_too_old': 'Questo nodo è più vecchio di questa pagina e non ha ancora impostazioni per questa applicazione. Aggiornalo per configurarla qui.',
'settings_node.directories_title': 'Directory delle app',
'settings_node.directories_hint': 'Cartelle condivise, e quale di esse le app Video, Musica e Foto usano come proprio/i punto/i di ingresso.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index a02e058..f2d94d9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -889,8 +889,6 @@ export default {
'settings_app.tmdb_token_prompt': 'TMDB に登録して、自分の API キーを発行してください。',
'settings_app.tmdb_token_link': 'キーを取得',
'settings_node.roots_offline_hint': 'ノードに接続していません — 変更はローカルノード経由で行われ、次回の再読み込みで反映されます。',
- 'settings_node.roots_node_too_old': 'このノードはこのページより古く、ディレクトリの表示はできますがここでの変更はできません。更新するか、meshbay-node root コマンドを使ってください。',
- 'settings_node.app_node_too_old': 'このノードはこのページより古く、このアプリの設定をまだ持っていません。ここで設定するには更新してください。',
'settings_node.directories_title': 'アプリのディレクトリ',
'settings_node.directories_hint': '共有フォルダと、動画・音楽・写真の各アプリがそれぞれの起点として使用するフォルダです。',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 0235d8a..f7f0775 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -907,8 +907,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Meld u aan bij TMDB om uw eigen API-sleutel te maken.',
'settings_app.tmdb_token_link': 'Sleutel ophalen',
'settings_node.roots_offline_hint': 'Niet verbonden met de node — wijzigingen gaan via de lokale node en worden bij de volgende herlaadbeurt actief.',
- 'settings_node.roots_node_too_old': 'Deze node is ouder dan deze pagina: hij kan zijn mappen tonen maar hier niet wijzigen. Werk hem bij, of gebruik de meshbay-node root-opdrachten.',
- 'settings_node.app_node_too_old': 'Deze node is ouder dan deze pagina en heeft nog geen instelling voor deze app. Werk hem bij om die hier in te stellen.',
'settings_node.directories_title': 'App-mappen',
'settings_node.directories_hint': 'Gedeelde mappen, en welke daarvan de Video\'s-, Muziek- en Foto\'s-apps als eigen startpunt(en) gebruiken.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index af4a5bb..8c50790 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -933,8 +933,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Zarejestruj się w TMDB, aby wygenerować własny klucz API.',
'settings_app.tmdb_token_link': 'Pobierz klucz',
'settings_node.roots_offline_hint': 'Brak połączenia z węzłem — zmiany przechodzą przez węzeł lokalny i zaczną działać po jego następnym przeładowaniu.',
- 'settings_node.roots_node_too_old': 'Ten węzeł jest starszy niż ta strona: może pokazać swoje katalogi, ale nie zmieni ich tutaj. Zaktualizuj go albo użyj poleceń meshbay-node root.',
- 'settings_node.app_node_too_old': 'Ten węzeł jest starszy niż ta strona i nie ma jeszcze ustawień tej aplikacji. Zaktualizuj go, aby skonfigurować ją tutaj.',
'settings_node.directories_title': 'Katalogi aplikacji',
'settings_node.directories_hint': 'Katalogi udostępnione oraz to, który z nich aplikacje Wideo, Muzyka i Zdjęcia traktują jako własny punkt (punkty) wejścia.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index f179c6f..8f903ba 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -892,8 +892,6 @@ export default {
'settings_app.tmdb_token_prompt': 'Cadastre-se no TMDB para gerar sua própria chave de API.',
'settings_app.tmdb_token_link': 'Obter uma chave',
'settings_node.roots_offline_hint': 'Sem conexão com o nó — as alterações passam pelo nó local e entram em vigor no próximo recarregamento.',
- 'settings_node.roots_node_too_old': 'Este nó é mais antigo que esta página: ele pode mostrar seus diretórios, mas não alterá-los aqui. Atualize-o ou use os comandos meshbay-node root.',
- 'settings_node.app_node_too_old': 'Este nó é mais antigo que esta página e ainda não tem configuração para este aplicativo. Atualize-o para configurá-lo aqui.',
'settings_node.directories_title': 'Diretórios de apps',
'settings_node.directories_hint': 'Pastas compartilhadas, e qual delas os apps Vídeos, Música e Fotos tratam como seu(s) próprio(s) ponto(s) de entrada.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index c89bbdc..c836de3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -876,8 +876,6 @@ export default {
'settings_app.tmdb_token_prompt': '在 TMDB 注册以生成你自己的 API 密钥。',
'settings_app.tmdb_token_link': '获取密钥',
'settings_node.roots_offline_hint': '未连接到节点 — 变更将通过本地节点进行,并在其下次重新加载时生效。',
- 'settings_node.roots_node_too_old': '该节点比本页面旧:它能显示自己的目录,但无法在此更改。请更新节点,或使用 meshbay-node root 命令。',
- 'settings_node.app_node_too_old': '该节点比本页面旧,尚不支持此应用的设置。请更新节点后在此配置。',
'settings_node.directories_title': '应用目录',
'settings_node.directories_hint': '共享文件夹,以及“视频”“音乐”和“照片”应用各自使用哪个(些)作为入口。',
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
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
index f590d20..18b99af 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
@@ -595,7 +595,7 @@ function SeasonMenu({ seasons, selected, selectedYear, onSelect }) {
// ── operator: correct a wrong automatic TMDB match ──────────────────────────
-// Same shape as group-settings.js's own signFn construction (setVideoRoot,
+// Same shape as group-settings.js's own signFn construction (setAppDirectories,
// setTmdbConfig, ...) — there is no group-wide "sign this" helper to share,
// each caller builds one from the connection it already has.
function buildSignFn(transportRef) {
diff --git a/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs b/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
index a6008c2..0fe7554 100644
--- a/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
+++ b/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
@@ -68,7 +68,6 @@ tp._channel = { readyState: 'open', bufferedAmount: 0, send() {}, close() {} };
tp._pc = { close() {} };
tp._gekRaw = hex(input.gek);
tp._connectArgs = { groupId: input.group_id };
-tp._nodeVersion = input.node_version;
const frames = [];
let uploadId = null;
diff --git a/packages/meshbay-hub/tests/test_app_settings_plugin.py b/packages/meshbay-hub/tests/test_app_settings_plugin.py
index b8afc23..892fd09 100644
--- a/packages/meshbay-hub/tests/test_app_settings_plugin.py
+++ b/packages/meshbay-hub/tests/test_app_settings_plugin.py
@@ -226,9 +226,9 @@ def test_the_page_performs_exactly_one_app_specific_operation():
# The page's own settings, which belong to no app: which apps are enabled
# at all, and how hard the node works watching its disk.
page_level = {"setAppsEnabled", "setScanSettings"}
- # Both are the same generic operation; the second is what a node too old
- # for it understands, chosen by version rather than by app.
- generic = {"setAppDirectories", "setAppDirectoriesLegacy"}
+ # One generic operation, keyed by the app's own name: adding an app adds
+ # no message type and no call site here.
+ generic = {"setAppDirectories"}
assert calls - page_level == generic, (
f"the settings page performs app-specific operations: "
f"{sorted(calls - page_level - generic)}")
diff --git a/packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py b/packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py
deleted file mode 100644
index b15018b..0000000
--- a/packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""
-The page ships before the nodes do.
-
-The SPA is served by the hub, so deploying the hub puts this version of the
-client in front of *every* node, including the ones still running MNP 1.0. That
-window is not a corner case — it is the normal state for as long as it takes an
-operator to update, and for a node someone else runs it may be indefinite.
-
-The failure mode is specific and quiet: a node logs an unknown message type and
-sends **nothing back**, so a control that speaks MNP 1.1 to it produces a
-thirty-second wait ending in a timeout, with nothing on screen to say the node
-simply cannot do this. Three of them were like that before these tests:
-
-* The shared-directories toggles, eject and plug have no older equivalent at
- all.
-* The per-app folder pickers spoke `app_directories`, where a 1.0 node
- understands `video_root` / `audio_root` / `photo_roots`.
-
-Source-reading, like the other SPA guards. What it cannot check is that the
-degraded path is pleasant; what it does check is that each of the three exists.
-"""
-
-import re
-from pathlib import Path
-
-import pytest
-
-STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
-TRANSPORT = STATIC / "transport.js"
-FILES_APP = STATIC / "files-app.js"
-GROUP_PAGE = STATIC / "group-page.js"
-GROUP_SETTINGS = STATIC / "group-settings.js"
-
-pytestmark = pytest.mark.skipif(not TRANSPORT.exists(),
- reason="SPA sources unavailable")
-
-
-def _component(source: str, name: str) -> str:
- start = source.index(f"\nfunction {name}(")
- end = source.find("\nfunction ", start + 1)
- return source[start:end if end != -1 else len(source)]
-
-
-# ── Knowing which node you are talking to ───────────────────────────────────
-
-def test_the_client_keeps_the_version_it_checked():
- """
- `_checkNodeVersion` parsed the node's version and threw it away, so nothing
- downstream could ask. Refusing to connect is not the only thing a version
- is good for.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- assert "this._nodeVersion = String(reply.v" in source
- assert "get supportsAppOps()" in source
-
-
-def test_the_capability_reads_the_version_rather_than_guessing():
- """
- Inferring it from whether some field happens to be present is how two
- unrelated things end up coupled — the flag would flip because a payload
- changed shape for another reason entirely.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- getter = source[source.index("get supportsAppOps()"):]
- getter = getter[:getter.index("\n }") + 4]
- assert "_nodeVersion" in getter
- assert "1" in getter, "no version comparison in the capability check"
-
-
-# ── The three degraded paths ────────────────────────────────────────────────
-
-def test_app_directories_fall_back_to_the_three_older_messages():
- """
- Videos, Music and Photos each had their own message before the generic op,
- and those still work — so an operator on an un-upgraded node keeps the
- ability they had rather than being handed a control that times out.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- legacy = source[source.index("async setAppDirectoriesLegacy("):]
- legacy = legacy[:legacy.index("\n async ", 1)]
- for call in ("setPhotoRoots", "setVideoRoot", "setAudioRoot"):
- assert call in legacy, f"{call} is not reachable on the older path"
-
- panel = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
- "GroupSettingsPanel")
- assert "transport.supportsAppOps" in panel, (
- "the settings page sends the 1.1 message unconditionally")
-
-
-def test_the_older_path_refuses_what_it_cannot_carry():
- """
- `video_root` and `audio_root` hold one folder. Sending several would store
- the first and drop the rest silently, which is worse than refusing — the
- operator would see a saved setting that is not what they chose.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- legacy = source[source.index("async setAppDirectoriesLegacy("):]
- legacy = legacy[:legacy.index("\n async ", 1)]
- assert "clean.length > 1" in legacy
- assert "throw new Error" in legacy
-
-
-def test_root_management_is_read_only_against_an_older_node():
- """
- Unlike the app directories, `writable`, `removable`, eject and plug have no
- older equivalent to route to. The controls are shown without being
- offered, with the reason, rather than accepting a click that goes nowhere.
- """
- panel = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
- "GroupSettingsPanel")
- table_call = panel[panel.index("<${SharedDirectoriesTable}"):]
- table_call = table_call[:table_call.index("/>")]
- assert "readOnly=" in table_call
- assert "nodeSupportsAppOps" in table_call
- assert "settings_node.roots_node_too_old" in panel, (
- "nothing says why the controls are inert")
-
-
-def test_chat_settings_are_hidden_rather_than_routed():
- """
- Chat's directory and link-preview switch are new in 1.1 with nothing
- before them, so there is no older message to fall back to.
- """
- panel = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
- "GroupSettingsPanel")
- assert "settings_node.app_node_too_old" in panel
-
-
-# ── Reading an older node's handshake ───────────────────────────────────────
-
-def test_the_ack_is_read_in_both_shapes(app=None):
- """
- A 1.0 ack has `video_root` and no `video_directories`, and no
- `chat_link_preview` at all. Reading a missing plural as "nothing
- configured" empties a working Videos tab; reading a missing switch as off
- silently changes what a group's chat does.
- """
- page = GROUP_PAGE.read_text(encoding="utf-8")
- block = page[page.index("setAppDirectories({"):]
- block = block[:block.index("setNodeSupportsAppOps")]
- for legacy in ("ack.video_root", "ack.audio_root", "ack.photo_roots"):
- assert legacy in block, f"{legacy} is not read as a fallback"
- assert "ack.chat_link_preview !== false" in page, (
- "an absent link-preview switch must read as on, not off")
diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py
index fe550f9..f2f4372 100644
--- a/packages/meshbay-hub/tests/test_transport_contracts.py
+++ b/packages/meshbay-hub/tests/test_transport_contracts.py
@@ -372,8 +372,11 @@ def test_the_upload_itself_is_sealed(transport):
"the destination is on the message in clear")
assert "...sealed," in sent or "...probeSealed," in sent, (
"the message must carry the sealed pair")
- assert "supportsSealedUpload" in body, (
- "an older node must be refused before a chunk is sent, not after")
+ # And no branch that sends anything else: an upload is sealed or it is not
+ # sent. A fallback here is a fallback the node would have to keep opening.
+ assert "filename: file.name" not in body.replace(
+ "msgpack_encode({ filename: file.name", ""), (
+ "a filename reaches the message outside the seal")
# ── MNP 1.0: the sealed handshake ack ────────────────────────────────────────
diff --git a/packages/meshbay-hub/tests/test_upload_seal_client.py b/packages/meshbay-hub/tests/test_upload_seal_client.py
index 2e4bfb5..d233ccd 100644
--- a/packages/meshbay-hub/tests/test_upload_seal_client.py
+++ b/packages/meshbay-hub/tests/test_upload_seal_client.py
@@ -80,7 +80,7 @@ def _node_session(tmp_path: Path, gek: bytes) -> WebRTCPeerSession:
def _probe_input(gek: bytes, mode: str, **extra) -> dict:
return {
"mode": mode, "gek": gek.hex(), "group_id": GROUP,
- "node_version": "2.0", "chunk_size": CHUNK, "dir": "library",
+ "chunk_size": CHUNK, "dir": "library",
"root": "library",
"file": {"name": "holiday.jpg", "data": BODY.hex()},
**extra,
@@ -156,19 +156,6 @@ def test_the_caller_is_told_the_name_the_node_chose(tmp_path, _gek, _sent):
assert result["stored"]["dir"] == "library"
-def test_the_client_refuses_an_older_node_before_sending_a_chunk(_gek):
- """
- A 1.x node would answer "Missing filename or data" — an error about the
- wrong thing, naming no upload, which fails every upload in flight. Asked
- first instead, and nothing goes on the wire.
- """
- result = _run_probe(_probe_input(_gek, "receive", acks=[],
- node_version="1.1"))
- assert result["state"] == "rejected"
- assert "older MeshBay" in result["message"]
- assert result["frames"] == [], "a chunk was sent to a node that cannot open it"
-
-
def test_an_interrupted_upload_resumes_where_the_node_stopped(tmp_path, _gek):
"""
The browser asks, the node answers, and the second attempt sends only what