aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ui
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-14 02:17:45 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-14 02:18:07 +0200
commit71df5857b213be893025c562977558ba79009c09 (patch)
tree1e84b347b4b1b2b4ebfdc738d46ad4f6803e2eec /packages/meshbay-node/src/meshbay_node/ui
parentd7120761fe8cf406f374ef769db6e1f9bf1fe287 (diff)
downloadmeshbay-71df5857b213be893025c562977558ba79009c09.tar.gz
fix(node): announce the node key in the challenge, and keep names in the roster
Both found by deploying the thing and running the workflow end to end. Neither was reachable from the test suite, for the same reason in each case: the tests knew something a real client cannot. 1. A first-time joiner had no way to learn node_pk. join_request signs a transcript naming the node, and the node key was only sent in handshake_ack — which an invited member cannot reach, having no GEK to prove. joinGroup() therefore threw "handshake incomplete" and the browser path for an invited member was broken. Every test built the transcript from a node key it already had, so nothing noticed. The challenge now carries node_pk. It is unverified at that point and never a substitute for the ack: the ack still proves possession and signs the transcript, the client checks the two values match and refuses a peer that changed identity mid-handshake, and TOFU pinning is unchanged. A wrong value only makes our own verification fail. test_invite_then_join_delivers_the_gek now takes the key from the challenge instead of from sk_node, so it proves a real client can learn it. 2. The roster pinned everyone without a name. `_do_join_request` took the username from the session, which takes it from the JWT — and the hub puts no username claim in a token. So identities were pinned with an empty name and `member revoke <name>` could never match: the live node answered "known: , ,". Invitations now carry the name (new invites.username column, with a migration for the roster DBs already out there), and the CLI resolves a name through the daemon: its own roster first, the hub as fallback for identities pinned before this. The harness that found them is QE/deploy/e2e.py — gitignored with the rest of QE/, so it is not in this commit. It does the SPA's job in Python against the live deployment: hub login, WebRTC via hub signaling, the unified handshake, joining with a code, index, chunk download and MSE segments. Verified against meshbay.org and the local node: an account registered from scratch is invited by code, receives the group key wrapped for a key it proved it holds, downloads and decrypts a file, streams 5 encrypted fMP4 segments, reconnects with no code, and is refused after `member revoke`. The node audit log shows invite_create → join_pinned(via=code) → gek_wrapped → handshake, then join_no_gek once revoked. Tests: 232 node+common. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py26
1 files changed, 26 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index 2f73868..28654df 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -243,6 +243,7 @@ def create_ui_app(state: dict) -> FastAPI:
role=ROLE_OPERATOR,
created_by="local-cli",
ttl=ttl,
+ username=(config.hub.username if config else ""),
)
invites = await roster.list_invites()
expires = next((i["expires_at"] for i in invites
@@ -292,6 +293,7 @@ def create_ui_app(state: dict) -> FastAPI:
role=ROLE_MEMBER,
created_by="local-cli",
ttl=ttl,
+ username=username,
)
invites = await roster.list_invites()
expires = next((i["expires_at"] for i in invites
@@ -300,6 +302,30 @@ def create_ui_app(state: dict) -> FastAPI:
return {"code": code, "expires_at": expires,
"username": username, "user_id": account["user_id"]}
+ @app.get("/api/resolve")
+ async def resolve_user(username: str):
+ """
+ Map a username to an account id for the CLI.
+
+ The roster answers first — it is the node's own record. The hub is the
+ fallback for identities pinned before invitations carried a name, and for
+ people admitted through an open-join group. Only an account id comes back;
+ no key is ever taken from here.
+ """
+ roster = state.get("roster")
+ if roster:
+ for ident in await roster.list_identities():
+ if ident["username"] == username:
+ return {"user_id": ident["user_id"], "source": "roster"}
+ hub = state.get("hub")
+ if hub and hub._session:
+ try:
+ account = await hub.get_user_pubkeys(username)
+ return {"user_id": account["user_id"], "source": "hub"}
+ except Exception:
+ pass
+ return JSONResponse({"error": f"Unknown user {username!r}"}, 404)
+
@app.post("/api/members/{user_id}/revoke")
async def revoke_member(user_id: str, group_id: str):
"""