summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-18 18:21:57 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-18 18:21:57 +0200
commitb86981f7b4ffe758136a527542ce256315823a46 (patch)
tree0d06a1cb5ca6d946d73db8724faa89bb1bd17501 /packages/meshbay-hub/src
parentcd2e89f5f5cccdb116db4fcb82d00b6325972782 (diff)
downloadmeshbay-b86981f7b4ffe758136a527542ce256315823a46.tar.gz
feat(node): the operator can close uploading to everyone but themselves
A group where every member may add files stays the default. Some groups want a library the operator curates, and until now the only way to get one was to designate no upload root at all — which refuses the operator too. **The node enforces it; the interface merely stops offering it.** The Upload button in the Files toolbar and the paperclip in the chat composer both disappear, which is a courtesy to the people who are not trying. The control is `_do_file_upload` refusing with `member_upload_off`, so a member on an old tab, or one speaking MNP directly, gets the same answer. There is a test for each, and the enforcement test is in the node package rather than beside the UI one so nobody reads the hidden button as the mechanism. **Changing it is a signed operator instruction** — `OP_MEMBER_UPLOAD`, on the same path as removing a member. An unsigned one would let any member turn it back on and make the setting a suggestion. The transcript's subject is `on` or `off`: what the operator is shown before signing has to name the outcome, not the operation. **It lives on the node**, in a new `group_settings` table in `roster.db`. Not the hub, which has no business deciding who may write to someone else's disk. Not `node.toml` either: that file is hand-written and full of comments recording decisions, `ops.py` appends to it rather than round-tripping it through a writer, and a setting toggled from a panel must not rewrite the operator's file or need a restart. The value is cached in the group context because the upload path is synchronous, and the signed operation updates both — storing it without applying it would make the panel say one thing while the node did another. **Absent means allowed**, at every layer: no row in the table, no key in the context, no field in `handshake_ack`. An older node and an older client both behave exactly as before, and upgrading never silently closes a group. Each of those three has its own test, because they fail independently. The operator is always exempt — otherwise turning it off locks them out of their own node with a config file and a restart as the only way back. `is_node_admin` was being computed in two places by then and is now one function, since two copies of "is this the operator" is how the ack and the gate come to disagree. A change reaches everyone already connected via `member_upload_ack`, so the button goes without a reconnection. That message is both a broadcast and the reply to the request that caused it, which is why the client does not return early on it. Docs updated for a cold start: draft-v6 §2.1b and change 9, a new "Where Phase 13 stands" section in CLAUDE.md recording what is built, deployed and still missing, the module map row, and desktop-client-v1 §10b on the Settings tab and where group settings live. 883 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js77
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js27
12 files changed, 162 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 62bbfa5..0843d1d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -1304,6 +1304,10 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
const [nodeRoots, setNodeRoots] = useState([]);
const [isNodeAdmin, setIsNodeAdmin] = useState(false);
+ // Whether ordinary members may upload here. The node decides and enforces it;
+ // this only says whether to offer the controls. Defaults to true so a node
+ // that predates the setting behaves as it always did.
+ const [memberUpload, setMemberUpload] = useState(true);
// Paired ≠ operator account. `is_node_admin` says the hub account owning this
// node is the one connecting; this says the node pinned *this browser's* key
// as an operator key. Only the second one lets you sign an invite, and only
@@ -1392,6 +1396,11 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
_pendingJoinCode = null;
if (cancelled) return;
setIsNodeAdmin(!!ack.is_node_admin);
+ setMemberUpload(ack.member_upload !== false);
+ // Changed while we are connected, by an operator who may be someone
+ // else entirely. Without this the button stays until a reconnection,
+ // and a button that is still there is a button people press.
+ transport.onUploadPolicy = (allowed) => setMemberUpload(allowed);
setOperatorPaired(transport.memberRole === 'operator');
// A first join to this node generated an identity for it; leave it with
@@ -1792,6 +1801,10 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
? selectedFiles[0] : null;
const deletableFiles = selectedFiles.filter(
e => isNodeAdmin || (userId && e.uploader_id === userId));
+
+ // Asked in two places — the Files toolbar and the chat composer — so it is
+ // answered once. The operator is never locked out of their own node.
+ const mayUpload = memberUpload || isNodeAdmin;
const run = (fn) => {
setSelecting(false);
setSelected(new Set());
@@ -1963,11 +1976,13 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
${tab === 'files' && status === 'connected' && html`
<div class="file-toolbar">
<div class="toolbar-group">
+ ${mayUpload && html`
<label class="tb-btn primary">
<${Icon} name="upload" /> ${t('group.upload')}
<input type="file" multiple style="display:none"
onChange=${uploadFile} />
</label>
+ `}
${canCreateDir && html`
<button class="tb-btn" onClick=${makeDirectory}>
<${Icon} name="folder-plus" /> ${t('group.mkdir')}
@@ -2085,6 +2100,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
${tab === 'chat' && status === 'connected' && html`
<${ChatPanel} transportRef=${transportRef} username=${username}
entries=${entries} gekRef=${gekRef} onRefreshIndex=${refreshIndex}
+ mayUpload=${mayUpload}
onPreview=${(entry) => {
if (entry.type === 'video') setVideoEntry(entry);
else setPreviewEntry(entry);
@@ -2099,6 +2115,8 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
transportRef=${transportRef} gekRef=${gekRef}
isNodeAdmin=${isNodeAdmin} userId=${userId}
operatorPaired=${operatorPaired} connected=${status === 'connected'}
+ memberUpload=${memberUpload}
+ onMemberUpload=${(allowed) => setMemberUpload(allowed)}
onLeft=${onLeft}
onPaired=${() => setOperatorPaired(true)} />
`}
@@ -2272,7 +2290,7 @@ function _b64ToU8(b64) {
*/
function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
isNodeAdmin, userId, operatorPaired, connected,
- onPaired, onLeft }) {
+ memberUpload, onMemberUpload, onPaired, onLeft }) {
const [members, setMembers] = useState([]);
const [adminId, setAdminId] = useState('');
const [loading, setLoading] = useState(true);
@@ -2350,6 +2368,37 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
}
}, [pairCode, transportRef, userId]);
+ const [uploadBusy, setUploadBusy] = useState(false);
+ const [uploadMsg, setUploadMsg] = useState('');
+
+ /**
+ * Close or open uploading for everyone who is not the operator.
+ *
+ * Signed, like removing a member: the node refuses an unsigned instruction,
+ * so this is a request to the node rather than a decision taken here. The
+ * button does not move until the node has said it did it.
+ */
+ const setUploads = useCallback(async (allowed) => {
+ const transport = transportRef && transportRef.current;
+ setUploadMsg('');
+ setUploadBusy(true);
+ try {
+ if (!transport || !transport.connected) {
+ throw new Error('Not connected to the node');
+ }
+ const sk = transport.sessionKeys && transport.sessionKeys.skEdB64;
+ const signFn = (sk && window.MeshBayKeys)
+ ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript)
+ : null;
+ await transport.setMemberUpload(allowed, signFn);
+ if (onMemberUpload) onMemberUpload(allowed);
+ } catch (err) {
+ setUploadMsg(err.message);
+ } finally {
+ setUploadBusy(false);
+ }
+ }, [transportRef, onMemberUpload]);
+
const [removing, setRemoving] = useState('');
/**
@@ -2504,6 +2553,27 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
</div>
`}
+ ${/* Operator only, and only with a live connection: the node is what
+ holds and enforces this, so there is nothing to show or change
+ without one. */ isNodeAdmin && connected && html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('members.uploads_title')}</h3>
+ <div class="settings-row">
+ <span class="settings-label">
+ ${memberUpload ? t('members.uploads_on') : t('members.uploads_off')}
+ </span>
+ <button class="admin-btn" disabled=${uploadBusy}
+ onClick=${() => setUploads(!memberUpload)}>
+ ${uploadBusy ? '...'
+ : (memberUpload ? t('members.uploads_disable')
+ : t('members.uploads_enable'))}
+ </button>
+ </div>
+ <p class="settings-hint">${t('members.uploads_hint')}</p>
+ ${uploadMsg && html`<p class="error-msg">${uploadMsg}</p>`}
+ </div>
+ `}
+
${connected && html`
<div class="settings-section">
<h3 class="settings-heading">${t('device.mine_title')}</h3>
@@ -2744,7 +2814,8 @@ function ChatImage({ filename, entries, transportRef, gekRef }) {
return html`<img class="chat-att-thumb" src=${blobUrl} alt=${filename} />`;
}
-function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, onPreview }) {
+function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex,
+ onPreview, mayUpload = true }) {
const [messages, setMessages] = useState([]);
const [hasMore, setHasMore] = useState(false);
const [loadingOlder, setLoadingOlder] = useState(false);
@@ -3041,11 +3112,13 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, on
</button>
`}
<div class="chat-input-row">
+ ${mayUpload && html`
<label class="chat-attach" title="${t('chat.attach')}">
${attaching ? html`<span class="spinner"></span>`
: html`<${Icon} name="clip" />`}
<input type="file" style="display:none" onChange=${attachFile} disabled=${attaching} />
</label>
+ `}
<textarea class="chat-input" rows="1"
placeholder="${t('chat.placeholder')}"
value=${input}
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 ddeaeeb..df72382 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -79,6 +79,12 @@ export default {
'group.tab_members': 'Mitglieder',
'group.tab_settings': "Einstellungen",
'members.danger_leave_hint': "Sie verlieren den Zugriff auf die Dateien und den Chat dieser Gruppe.",
+ 'members.uploads_title': "Uploads",
+ 'members.uploads_on': "Mitglieder können Dateien hinzufügen",
+ 'members.uploads_off': "Nur Sie können Dateien hinzufügen",
+ 'members.uploads_disable': "Ausschalten",
+ 'members.uploads_enable': "Einschalten",
+ 'members.uploads_hint': "Gilt für alle außer Ihnen. Der Knoten lehnt den Upload selbst ab — es geht nicht darum, eine Schaltfläche zu verbergen.",
'members.danger_delete_hint': "Die Gruppe verschwindet für alle Mitglieder. Das lässt sich nicht rückgängig machen.",
'group.filter': 'Dateien filtern …',
'group.col_name': 'Name',
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 9a0f754..ce14f0b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -80,6 +80,12 @@ export default {
'group.tab_members': 'Members',
'group.tab_settings': "Settings",
'members.danger_leave_hint': "You will lose access to this group's files and chat.",
+ 'members.uploads_title': "Uploads",
+ 'members.uploads_on': "Members can add files",
+ 'members.uploads_off': "Only you can add files",
+ 'members.uploads_disable': "Turn off",
+ 'members.uploads_enable': "Turn on",
+ 'members.uploads_hint': "Applies to everyone but you. The node refuses the upload itself, so this is not a matter of hiding a button.",
'members.danger_delete_hint': "This removes the group for every member. It cannot be undone.",
'group.filter': 'Filter files...',
'group.col_name': 'Name',
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 0c72445..2d9084b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -77,6 +77,12 @@ export default {
'group.tab_members': 'Miembros',
'group.tab_settings': "Ajustes",
'members.danger_leave_hint': "Perderá el acceso a los archivos y al chat de este grupo.",
+ 'members.uploads_title': "Subidas",
+ 'members.uploads_on': "Los miembros pueden añadir archivos",
+ 'members.uploads_off': "Solo usted puede añadir archivos",
+ 'members.uploads_disable': "Desactivar",
+ 'members.uploads_enable': "Activar",
+ 'members.uploads_hint': "Se aplica a todos menos a usted. El nodo rechaza la subida por sí mismo: no se trata de ocultar un botón.",
'members.danger_delete_hint': "El grupo desaparece para todos sus miembros. No se puede deshacer.",
'group.filter': 'Filtrar archivos...',
'group.col_name': 'Nombre',
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 0dec5cf..9aa2857 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -78,6 +78,12 @@ export default {
'group.tab_members': 'Membres',
'group.tab_settings': "Paramètres",
'members.danger_leave_hint': "Vous perdrez l’accès aux fichiers et à la discussion de ce groupe.",
+ 'members.uploads_title': "Envois de fichiers",
+ 'members.uploads_on': "Les membres peuvent ajouter des fichiers",
+ 'members.uploads_off': "Vous seul pouvez ajouter des fichiers",
+ 'members.uploads_disable': "Désactiver",
+ 'members.uploads_enable': "Activer",
+ 'members.uploads_hint': "S’applique à tout le monde sauf vous. C’est le nœud qui refuse l’envoi : il ne s’agit pas de masquer un bouton.",
'members.danger_delete_hint': "Le groupe disparaît pour tous ses membres. C’est irréversible.",
'group.filter': 'Filtrer les fichiers...',
'group.col_name': 'Nom',
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 81b6a3a..379e7cb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -78,6 +78,12 @@ export default {
'group.tab_members': 'Membri',
'group.tab_settings': "Impostazioni",
'members.danger_leave_hint': "Perderai l’accesso ai file e alla chat di questo gruppo.",
+ 'members.uploads_title': "Caricamenti",
+ 'members.uploads_on': "I membri possono aggiungere file",
+ 'members.uploads_off': "Solo tu puoi aggiungere file",
+ 'members.uploads_disable': "Disattiva",
+ 'members.uploads_enable': "Attiva",
+ 'members.uploads_hint': "Vale per tutti tranne te. È il nodo a rifiutare il caricamento: non si tratta di nascondere un pulsante.",
'members.danger_delete_hint': "Il gruppo scompare per tutti i membri. Non è reversibile.",
'group.filter': 'Filtra i file...',
'group.col_name': 'Nome',
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 11c6e31..45e7aa5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -76,6 +76,12 @@ export default {
'group.tab_members': 'メンバー',
'group.tab_settings': "設定",
'members.danger_leave_hint': "このグループのファイルとチャットにアクセスできなくなります。",
+ 'members.uploads_title': "アップロード",
+ 'members.uploads_on': "メンバーはファイルを追加できます",
+ 'members.uploads_off': "あなただけがファイルを追加できます",
+ 'members.uploads_disable': "無効にする",
+ 'members.uploads_enable': "有効にする",
+ 'members.uploads_hint': "あなた以外の全員に適用されます。ノード自身がアップロードを拒否するため、ボタンを隠すだけの話ではありません。",
'members.danger_delete_hint': "グループはすべてのメンバーから消えます。元に戻せません。",
'group.filter': 'ファイルを絞り込み…',
'group.col_name': '名前',
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 1af0eb1..37d3e24 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -79,6 +79,12 @@ export default {
'group.tab_members': 'Leden',
'group.tab_settings': "Instellingen",
'members.danger_leave_hint': "U verliest de toegang tot de bestanden en de chat van deze groep.",
+ 'members.uploads_title': "Uploads",
+ 'members.uploads_on': "Leden kunnen bestanden toevoegen",
+ 'members.uploads_off': "Alleen u kunt bestanden toevoegen",
+ 'members.uploads_disable': "Uitschakelen",
+ 'members.uploads_enable': "Inschakelen",
+ 'members.uploads_hint': "Geldt voor iedereen behalve u. De node weigert de upload zelf — het gaat niet om het verbergen van een knop.",
'members.danger_delete_hint': "De groep verdwijnt voor alle leden. Dit kan niet ongedaan worden gemaakt.",
'group.filter': 'Bestanden filteren...',
'group.col_name': 'Naam',
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 f57dc90..6766515 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -83,6 +83,12 @@ export default {
'group.tab_members': 'Członkowie',
'group.tab_settings': "Ustawienia",
'members.danger_leave_hint': "Utracisz dostęp do plików i czatu tej grupy.",
+ 'members.uploads_title': "Przesyłanie plików",
+ 'members.uploads_on': "Członkowie mogą dodawać pliki",
+ 'members.uploads_off': "Tylko Ty możesz dodawać pliki",
+ 'members.uploads_disable': "Wyłącz",
+ 'members.uploads_enable': "Włącz",
+ 'members.uploads_hint': "Dotyczy wszystkich poza Tobą. To węzeł odrzuca przesłanie — nie chodzi o ukrycie przycisku.",
'members.danger_delete_hint': "Grupa zniknie dla wszystkich członków. Tego nie można cofnąć.",
'group.filter': 'Filtruj pliki...',
'group.col_name': 'Nazwa',
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 0f1586f..fa46887 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
@@ -79,6 +79,12 @@ export default {
'group.tab_members': 'Membros',
'group.tab_settings': "Configurações",
'members.danger_leave_hint': "Você perderá o acesso aos arquivos e ao chat deste grupo.",
+ 'members.uploads_title': "Envios",
+ 'members.uploads_on': "Os membros podem adicionar arquivos",
+ 'members.uploads_off': "Somente você pode adicionar arquivos",
+ 'members.uploads_disable': "Desativar",
+ 'members.uploads_enable': "Ativar",
+ 'members.uploads_hint': "Vale para todos menos você. O nó recusa o envio por conta própria: não se trata de esconder um botão.",
'members.danger_delete_hint': "O grupo desaparece para todos os membros. Não há como desfazer.",
'group.filter': 'Filtrar arquivos...',
'group.col_name': 'Nome',
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 89927bb..3276db8 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
@@ -76,6 +76,12 @@ export default {
'group.tab_members': '成员',
'group.tab_settings': "设置",
'members.danger_leave_hint': "您将无法再访问该群组的文件和聊天。",
+ 'members.uploads_title': "上传",
+ 'members.uploads_on': "成员可以添加文件",
+ 'members.uploads_off': "只有您可以添加文件",
+ 'members.uploads_disable': "关闭",
+ 'members.uploads_enable': "开启",
+ 'members.uploads_hint': "适用于除您之外的所有人。节点自身会拒绝上传,并非只是隐藏按钮。",
'members.danger_delete_hint': "该群组将对所有成员消失,且无法恢复。",
'group.filter': '筛选文件…',
'group.col_name': '名称',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 853ae75..a0509a0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -103,6 +103,7 @@ class MeshBayTransport {
set onStreamEnd(fn) { this._onStreamEnd = fn; }
set onStreamError(fn) { this._onStreamError = fn; }
set onIndexSync(fn) { this._onIndexSync = fn; }
+ set onUploadPolicy(fn) { this._onUploadPolicy = fn; }
get sessionKeys() { return this._sessionKeys; }
@@ -592,6 +593,24 @@ class MeshBayTransport {
* Only the node can do this: its roster decides who it serves. Removing them
* on the hub is the other half, and neither implies the other.
*/
+ /**
+ * Turn uploading by ordinary members on or off.
+ *
+ * Signed by the operator like any other privileged operation — the node
+ * refuses an unsigned one, which is what stops a member turning it back on.
+ */
+ async setMemberUpload(allowed, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'member_upload', v: '0.1', allowed: Boolean(allowed),
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(
+ msg, 'member_upload', allowed ? 'on' : 'off', signFn);
+ }
+ return msg;
+ }
+
async revokeMember(userId, signFn) {
const msg = await this._sendAndWait({
type: 'member_revoke', v: '0.1', user_id: userId,
@@ -1079,6 +1098,14 @@ class MeshBayTransport {
return;
}
+ // The operator changed who may upload. Unsolicited: it arrives at everyone
+ // connected, not only at whoever asked. It still has to reach a pending
+ // caller — the operator's own request resolves on this reply — so it falls
+ // through to the matching below rather than returning here.
+ if (msg.type === 'member_upload_ack' && this._onUploadPolicy) {
+ this._onUploadPolicy(Boolean(msg.allowed));
+ }
+
if (msg.type === 'index_sync' && msg.entries) {
if (this._onIndexSync) this._onIndexSync(msg);
const oldest = this._pending.entries().next();