diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-15 10:17:02 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-15 10:17:02 +0200 |
| commit | 0503682c0e2add135b88c2a1fadfe07455680a71 (patch) | |
| tree | f4dce260c9875dc4573d04ad553834ad36df3305 /packages/meshbay-node/tests | |
| parent | 4c4fe55bc17ea2223a52b31d2be5b762af8d75bf (diff) | |
| download | meshbay-0503682c0e2add135b88c2a1fadfe07455680a71.tar.gz | |
feat(node): meshbay-node group add — host another of your groups
Attaching a group to a node meant hand-editing node.toml with a UUID
copied from a browser URL, restarting, and knowing that gek-init exists.
Nothing in the CLI said so, and on a node reached over SSH there is no
paste buffer to carry a UUID across in the first place.
meshbay-node group add grenet --dir ~/grenet-share
The name is resolved against the operator's groups on the hub by the
daemon, which is the process holding the session. The [[groups]] block is
appended to node.toml as text rather than round-tripped through a TOML
writer: the file is hand-written and its comments explain decisions worth
keeping. The directory is created, and the command says what remains —
restart, then gek-init for that group.
It refuses a name it cannot find by printing the groups it can, with
their ids. That listing is the useful half of the answer and it was
missing everywhere: _daemon_api now renders an `available` list from any
endpoint that offers one.
The key is per group and pairing is not, which is the part that reads as
a gap until it is written down: one paired browser covers every group the
node hosts, while each group's key admits only its own members. §4 of the
user guide now says all three of those in one place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_roster_pairing.py | 49 |
1 files changed, 49 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py index a2f7cd1..a5a48e4 100644 --- a/packages/meshbay-node/tests/test_roster_pairing.py +++ b/packages/meshbay-node/tests/test_roster_pairing.py @@ -756,3 +756,52 @@ def test_admin_authority_is_never_fetched_from_the_hub(): assert "admin_pk_ed25519" not in daemon, ( "the node.toml operator key is gone; it must not come back as a second " "source of authority") + + +# ── Hosting another group ──────────────────────────────────────────────────── + +async def test_group_add_appends_without_rewriting_the_file(tmp_path): + """ + node.toml is hand-written and full of comments explaining decisions. The + block is appended as text for that reason: a round trip through a TOML + writer would silently throw all of it away. + """ + from meshbay_node.config import load_config + + conf = tmp_path / "node.toml" + conf.write_text( + '# keep me\n[hub]\nurl = "https://meshbay.org"\nusername = "grenet"\n\n' + '[[groups]]\nid = "aaaa"\nname = "first"\nshared_dir = "/tmp/a"\n') + + block = ('\n[[groups]]\n' + 'id = "bbbb"\n' + 'name = "second"\n' + 'shared_dir = "/tmp/b"\n' + 'visibility = "private"\n') + with conf.open("a") as f: + f.write(block) + + assert "# keep me" in conf.read_text(), "comments must survive" + cfg = load_config(conf) + assert [g.name for g in cfg.groups] == ["first", "second"] + assert [g.shared_dir for g in cfg.groups] == ["/tmp/a", "/tmp/b"] + + +async def test_each_group_gets_its_own_key(tmp_path, roster): + """ + Two groups on one node are two separate memberships and two separate keys: + being admitted to one must say nothing about the other. This is the property + that makes hosting a second group meaningful rather than cosmetic. + """ + from meshbay_common.crypto import generate_gek + + gek_a, gek_b = generate_gek(), generate_gek() + assert gek_a != gek_b + + sk_ed, pk_ed_b64, pk_x_b64 = _keypair() + await roster.pin_identity("member", "member", pk_ed_b64, pk_x_b64, "code") + await roster.set_member("group-a", "member", ROLE_MEMBER, "active", "op") + + assert await roster.is_authorized("group-a", "member") is True + assert await roster.is_authorized("group-b", "member") is False, ( + "membership of one group must not admit anyone to another") |