From 462d76898a306981fbeac859cd54da1468e80639 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 7 Oct 2026 22:12:54 +0200 Subject: fix(node): name members admitted without an invitation name A member who joined by link, by a new device or into an open group was pinned in the roster with no name, so the audit log showed only the first characters of their id. The hub's MNP token now carries the account's username, and after the handshake the node writes it into the roster for an account whose name is empty. An invitation's name is never overwritten; the name is a label, authority stays on `sub`. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-hub/src/meshbay_hub/api/nodes.py | 3 ++- packages/meshbay-hub/src/meshbay_hub/auth.py | 9 ++++++++- packages/meshbay-hub/tests/test_mnp_token.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) (limited to 'packages/meshbay-hub') diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py index b158621..b35f11e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py @@ -64,7 +64,8 @@ async def mnp_token( # `not_a_member`, which is the answer that case has always had. "mnp_token": issue_mnp_token(current_user.id, groups=[group_id] if member else [], - node_pk=(body.node_pk if body else "")), + node_pk=(body.node_pk if body else ""), + username=current_user.username), "expires_in": 900, } diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index 1d09581..41fb159 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -241,7 +241,8 @@ def issue_access_token( def issue_mnp_token(user_id: str, groups: list[str] | None = None, - node_pk: str | None = None, ttl: int = 900) -> str: + node_pk: str | None = None, ttl: int = 900, + username: str = "") -> str: """Issue the short-lived token a member presents to a node in the handshake. `aud=MNP_AUD`, so it is accepted by `authorize_token` and refused by the hub @@ -254,6 +255,11 @@ def issue_mnp_token(user_id: str, groups: list[str] | None = None, cannot be replayed to another node the member also belongs to — the node checks it in `authorize_token` (E10). The client knows the target node's key before it connects and asks for a token bound to it. + + `username` is the account's name, so the node can show a person by name in + its audit log and roster. Admission by link, by device or into an open group + carries no name of its own; without this the node knew those members only + by id. It is a label, never authority: the node decides on `sub`. """ if _hub_sk_pem is None: raise RuntimeError("Hub keypair not loaded") @@ -261,6 +267,7 @@ def issue_mnp_token(user_id: str, groups: list[str] | None = None, payload = { "iss": _hub_id, "sub": user_id, + "username": username, "jti": str(uuid.uuid4()), "iat": now, "exp": now + ttl, diff --git a/packages/meshbay-hub/tests/test_mnp_token.py b/packages/meshbay-hub/tests/test_mnp_token.py index ef6423d..71d7ff6 100644 --- a/packages/meshbay-hub/tests/test_mnp_token.py +++ b/packages/meshbay-hub/tests/test_mnp_token.py @@ -136,3 +136,17 @@ async def test_a_token_must_name_its_group(client): r = await client.post("/v1/nodes/mnp-token", json={}, headers={"Authorization": f"Bearer {tok}"}) assert r.status_code == 422 + + +@pytest.mark.asyncio +async def test_mnp_token_names_the_account(client): + """A member admitted by link has no name in the node's roster but this one; + without it the node's audit log shows a bare id.""" + from meshbay_hub.auth import hub_public_key_pem + + tok = await _session_token(client, "mnp_named") + gid = await _own_group(client, tok) + mnp = (await client.post("/v1/nodes/mnp-token", json={"group_id": gid}, + headers={"Authorization": f"Bearer {tok}"})).json()["mnp_token"] + peer = authorize_token(mnp, hub_public_key_pem(), group_id=gid) + assert peer.username == "mnp_named" -- cgit v1.2.3