summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_daemon.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-03 16:16:55 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-03 16:16:55 +0200
commit675beed6ff688733a9598f9d82d41578f48316be (patch)
tree78dd4f8dff312f0ad99bd63bc679bf402591c5ed /packages/meshbay-node/tests/test_daemon.py
parent15087b0e8fdb872602310119f14680aaa443fd93 (diff)
downloadmeshbay-675beed6ff688733a9598f9d82d41578f48316be.tar.gz
feat!: MNP 1.0 — seal index and handshake_ack under the group key
`index_sync`, `index_delta` and the `handshake_ack` config payload now travel sealed under a GEK-derived subkey (`meshbay_common/groupbox.py`, mirrored by `sealGroup`/`openGroup` in `crypto.js`). Only `type`, `v`, `group_id` and the ack's `node_pk`/`proof`/`sig` stay in clear — a receiver must route and authenticate before it would trust a decryption. Verify, then decrypt. The ack line is integrity, not confidentiality: the signed handshake transcript names no ack field, so `is_node_admin`, `enabled_apps`, `video_root` and the rest were authenticated by the DTLS channel alone. The index line is defence in depth against a repeat of C1/C6 — a peer served before the handshake completes now gets ciphertext, not filenames. Nothing against an observer, the hub, or a member; that is the whole claim. `index_progress` stays clear (D3, counters only). Chat is out of scope. Failure is fatal: a payload that does not open ends the session naming the message type — never an empty index or an empty `enabled_apps`, both of which are legitimate states. Version negotiation ships here too (phase 15.6, brought forward): `v` + `v_min` on `handshake` and `handshake_challenge`, refused with `version_too_old` / `version_too_new` / `version_unreadable`. The flag day was already being paid for; the next breaking change now costs a refusal message. BREAKING CHANGE: breaks the WebRTC wire every deployed client speaks. Hub and every node must deploy together; the SPA is served by the hub, so a browser picks up the new client on reload. See MESHBAY_NODE_PROTOCOL.md §11.1a, §13.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HkzbhmMmK8PqQBtGz5zCvY
Diffstat (limited to 'packages/meshbay-node/tests/test_daemon.py')
-rw-r--r--packages/meshbay-node/tests/test_daemon.py18
1 files changed, 12 insertions, 6 deletions
diff --git a/packages/meshbay-node/tests/test_daemon.py b/packages/meshbay-node/tests/test_daemon.py
index 71aae78..8bd169d 100644
--- a/packages/meshbay-node/tests/test_daemon.py
+++ b/packages/meshbay-node/tests/test_daemon.py
@@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from unittest.mock import AsyncMock, MagicMock, patch
from meshbay_common.crypto import generate_gek
+from meshbay_common.groupbox import PURPOSE_INDEX, unseal
from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig
from conftest import one_root
from meshbay_node.daemon import NodeDaemon
@@ -263,7 +264,8 @@ async def test_daemon_index_change_pushes_to_peers(tmp_path, shared_dir, gek, hu
msg = mock_session._send.call_args[0][0]
assert msg["type"] == "index_sync"
assert msg["group_id"] == "a" * 32
- assert len(msg["entries"]) == indexer.index.count
+ payload = unseal(gek, PURPOSE_INDEX, "index_sync", "a" * 32, msg)
+ assert len(payload["entries"]) == indexer.index.count
# Finding H7: this group is private, so its content hashes must NOT be
# registered with the hub. The test previously asserted the opposite —
@@ -391,7 +393,9 @@ async def test_first_broadcast_is_full_sync_second_is_delta(tmp_path, shared_dir
await asyncio.sleep(0.05)
first = session._send.call_args_list[0].args[0]
assert first["type"] == "index_sync"
- assert len(first["entries"]) == indexer.index.count
+ # Sealed since MNP 1.0 — the entries are inside, not on the envelope.
+ first_payload = unseal(gek, PURPOSE_INDEX, "index_sync", "a" * 32, first)
+ assert len(first_payload["entries"]) == indexer.index.count
# Nothing actually changed in the index between the two calls, but
# _on_index_change does not know or care why it was called — the
@@ -401,8 +405,9 @@ async def test_first_broadcast_is_full_sync_second_is_delta(tmp_path, shared_dir
await asyncio.sleep(0.05)
second = session._send.call_args_list[1].args[0]
assert second["type"] == "index_delta"
- assert second["additions"] == []
- assert second["deletions"] == []
+ second_payload = unseal(gek, PURPOSE_INDEX, "index_delta", "a" * 32, second)
+ assert second_payload["additions"] == []
+ assert second_payload["deletions"] == []
@pytest.mark.asyncio
@@ -435,8 +440,9 @@ async def test_delta_reflects_additions_and_deletions(tmp_path, shared_dir, gek)
delta_msg = session._send.call_args_list[1].args[0]
assert delta_msg["type"] == "index_delta"
- assert delta_msg["deletions"] == [removed_id]
- assert [a["id"] for a in delta_msg["additions"]] == ["new-file-id"]
+ payload = unseal(gek, PURPOSE_INDEX, "index_delta", "a" * 32, delta_msg)
+ assert payload["deletions"] == [removed_id]
+ assert [a["id"] for a in payload["additions"]] == ["new-file-id"]
@pytest.mark.asyncio