aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py12
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py18
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py13
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py26
4 files changed, 55 insertions, 14 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 930dabc..58fa99a 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -835,15 +835,9 @@ def main() -> None:
return
# revoke and unpin both name a person; the daemon resolves the account.
- roster_out = _daemon_api(cfg, "/api/roster")
- match = next((i for i in roster_out.get("identities", [])
- if i["username"] == args.target), None)
- if not match:
- known = ", ".join(i["username"]
- for i in roster_out.get("identities", []))
- print(f"{args.target!r} is not pinned on this node")
- print(f"known: {known or 'nobody yet'}")
- sys.exit(1)
+ # It tries its own roster first and falls back to the hub, so a node that
+ # pinned someone before invitations carried a name is still manageable.
+ match = _daemon_api(cfg, f"/api/resolve?username={args.target}")
if sub == "revoke":
group_id = _resolve_group(cfg, args.group)
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index dab1497..6bda56b 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -77,6 +77,7 @@ CREATE TABLE IF NOT EXISTS invites (
code_hash TEXT PRIMARY KEY,
group_id TEXT NOT NULL,
user_id TEXT NOT NULL,
+ username TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL,
@@ -142,6 +143,15 @@ class Roster:
# WAL: the CLI writes invites (`operator pair`) while the daemon reads them.
await self._db.execute("PRAGMA journal_mode=WAL")
await self._db.executescript(_SCHEMA)
+ # invites.username was added after the first deployments: the name is what
+ # the operator types, and it cannot be recovered from the JWT because the
+ # hub does not put one there. CREATE TABLE IF NOT EXISTS will not add a
+ # column to a table that already exists.
+ async with self._db.execute("PRAGMA table_info(invites)") as cur:
+ columns = {r[1] for r in await cur.fetchall()}
+ if "username" not in columns:
+ await self._db.execute(
+ "ALTER TABLE invites ADD COLUMN username TEXT NOT NULL DEFAULT ''")
await self._db.commit()
async def close(self) -> None:
@@ -289,6 +299,7 @@ class Roster:
role: str,
created_by: str,
ttl: int = DEFAULT_INVITE_TTL,
+ username: str = "",
) -> str:
"""
Issue a one-time code. Returns it in the clear — this is the only moment it
@@ -305,10 +316,9 @@ class Roster:
code = generate_code()
expires = datetime.now(timezone.utc) + timedelta(seconds=ttl)
await self._db.execute(
- "INSERT INTO invites "
- "(code_hash, group_id, user_id, role, created_by, created_at, expires_at) "
- "VALUES (?, ?, ?, ?, ?, ?, ?)",
- (hash_code(code), group_id, user_id, role, created_by, _now(),
+ "INSERT INTO invites (code_hash, group_id, user_id, username, role, "
+ "created_by, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ (hash_code(code), group_id, user_id, username, role, created_by, _now(),
expires.isoformat(timespec="seconds")),
)
await self._db.commit()
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 10dfcb0..416e84c 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -390,6 +390,13 @@ class WebRTCPeerSession:
"type": MNP.HANDSHAKE_CHALLENGE,
"v": MNP_VERSION,
"nonce": base64.b64encode(self._gek_challenge).decode(),
+ # Announced here because a first-time joiner needs it *before* the
+ # ack: join_request signs a transcript naming this node, and someone
+ # who has never held the GEK cannot complete the handshake to learn
+ # it. Unverified at this point — the ack proves it, the client checks
+ # the two match, and a wrong value only makes our own verification
+ # fail. It is never a substitute for the ack's proof and signature.
+ "node_pk": self._node_pk_b64(),
})
def _do_handshake_response(self, msg: dict) -> None:
@@ -731,7 +738,10 @@ class WebRTCPeerSession:
return
await self._pin_and_admit(
- roster, user_id, username, pk_ed_b64, pk_x_b64,
+ # The name comes from the invitation, not from the token: the hub does
+ # not put a username claim in a JWT, so pinning from the session alone
+ # left the roster nameless and `member revoke <name>` unable to match.
+ roster, user_id, invite["username"] or username, pk_ed_b64, pk_x_b64,
group_id=invite["group_id"], role=invite["role"],
approved_by=invite["created_by"], via="code")
await self._join_ok(user_id, pk_x_raw, invite["group_id"],
@@ -1332,6 +1342,7 @@ class WebRTCPeerSession:
role=ROLE_MEMBER,
created_by=self._user_id or "",
ttl=self._ctx.get("invite_ttl", DEFAULT_INVITE_TTL),
+ username=payload.get("username", ""),
)
invites = await roster.list_invites()
expires = next(
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):
"""