diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-07 22:12:54 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-07 22:20:45 +0200 |
| commit | 462d76898a306981fbeac859cd54da1468e80639 (patch) | |
| tree | 9a49723da751b4a7225e3dd37181ecceed192f3a /packages/meshbay-hub | |
| parent | 92e6b9823119b5461efc304a81e79e186a928e6d (diff) | |
| download | meshbay-462d76898a306981fbeac859cd54da1468e80639.tar.gz | |
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/nodes.py | 3 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/auth.py | 9 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_mnp_token.py | 14 |
3 files changed, 24 insertions, 2 deletions
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" |