diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-14 21:45:53 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-14 21:45:53 +0200 |
| commit | cd2745cecff12e894e0dfa702bff6a90f0e8734e (patch) | |
| tree | 8e864603cfd4c49cde535c8c4f96c5151ed4276c /packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | |
| parent | df3b808792daa745b5b0d5b9896ddca8849fe8b1 (diff) | |
| download | meshbay-cd2745cecff12e894e0dfa702bff6a90f0e8734e.tar.gz | |
feat: a group can be left out of Search, and Search tries every node
`search_listed` is a per-group setting on the node, changed by a signed
operator op and carried in the sealed handshake ack. Search reads it after
the handshake and stops there: no index is fetched, cached or merged, in any
of the four views, and the page says how many groups it left out. The switch
is a "Search" section in the group's settings, shown to the operator.
Absent means listed, at every layer: roster default, ack default, and the
client only drops a group on an explicit `false` — so an upgrade or an older
node removes nothing from anyone's Search.
It is a listing preference and protects nothing: the node serves the same
index to Search and to the group page and cannot tell them apart, every
member lists the group by opening it, and a client that ignores the flag
lists it in Search too. Design §9.11 says so, so it is never described as
private. The cost is one handshake per unlisted group, because only the node
knows the setting.
Search also took `nodes[0]` twice — for the index and for the pooled
connection — the defect 4cce50f fixed on the group page only. One
`connectToGroup` now walks the list the same way: a refusal about this
browser stops, `not_hosted` or a failed connection moves on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XuNrwLf5EFWCMHzfoEvnpm
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 43 |
1 files changed, 43 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 79a97cc..af41061 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -79,6 +79,7 @@ from meshbay_common.adminop import ( OP_CHAT_DIRECTORY, OP_CHAT_EPOCH, OP_CHAT_LINK_PREVIEW, + OP_SEARCH_LISTED, OP_ROOT_ADD, OP_ROOT_REMOVE, OP_ROOT_UPDATE, @@ -623,6 +624,8 @@ class WebRTCPeerSession: self._do_chat_directory(msg) elif mtype == MNP.CHAT_LINK_PREVIEW: self._do_chat_link_preview(msg) + elif mtype == MNP.SEARCH_LISTED: + self._do_search_listed(msg) elif mtype == MNP.CHAT_EPOCH: self._do_chat_epoch(msg) elif mtype == MNP.CHAT_KEYS_REQ: @@ -949,6 +952,11 @@ class WebRTCPeerSession: # on, which is what it did before this existed. "chat_link_preview": bool( self._group_ctx().get("chat_link_preview", True)), + # Whether the reader's cross-group Search should list this group. + # Presentation only: the index below is served to Search and to the + # group page alike, and this cannot tell them apart. Sealed like the + # rest, so the hub cannot flip it. Absent means listed. + "search_listed": bool(self._group_ctx().get("search_listed", True)), # Which chat epoch key a client should be sealing under. Inside # the sealed part of the ack like every other configuration field, # so it carries an authentication tag from a key the hub does not @@ -2392,6 +2400,38 @@ class WebRTCPeerSession: self._broadcast_to_group({"type": MNP.CHAT_LINK_PREVIEW_ACK, "v": MNP_VERSION, "enabled": enabled}) + def _do_search_listed(self, msg: dict) -> None: + """ + Whether this group's files appear in members' cross-group Search. + Signed because it changes what every member's Search shows, not + because it protects anything — see ops.set_search_listed. + """ + listed = msg.get("listed") + if not isinstance(listed, bool): + self._send({"type": "error", "detail": "Missing or invalid 'listed'"}) + return + if not self._has_admin_authority(): + self._send({"type": "error", "detail": "No authorized key for this"}) + return + self._issue_admin_challenge(OP_SEARCH_LISTED, "on" if listed else "off") + + async def _admin_exec_search_listed( + self, pending: dict, transcript: bytes, sig: bytes, + ) -> None: + listed = pending["subject"] == "on" + if not await self._verify_admin_sig(transcript, sig): + self._send({"type": "error", "detail": "Signature verification failed"}) + self._audit("admin_auth_failed", f"search_listed:{pending['subject']}") + return + try: + await self._run_op(ops.set_search_listed, self._group_id or "", listed) + except ops.OpError as e: + self._send({"type": "error", "detail": e.message}) + return + self._audit("search_listed", pending["subject"]) + self._broadcast_to_group({"type": MNP.SEARCH_LISTED_ACK, + "v": MNP_VERSION, "listed": listed}) + def _do_chat_epoch(self, msg: dict) -> None: """ Open a new chat epoch by hand. Operator only, and signed. @@ -5446,6 +5486,9 @@ class WebRTCPeerSession: elif pending["op"] == OP_CHAT_LINK_PREVIEW: self._spawn( self._admin_exec_chat_link_preview(pending, transcript, sig_bytes)) + elif pending["op"] == OP_SEARCH_LISTED: + self._spawn( + self._admin_exec_search_listed(pending, transcript, sig_bytes)) elif pending["op"] == OP_CHAT_EPOCH: self._spawn( self._admin_exec_chat_epoch(pending, transcript, sig_bytes)) |