summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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