From 2e57173ed9f2627b3efa3eb6f2ed922c5dfc071d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sat, 2 May 2026 23:08:50 +0100 Subject: [PATCH] fix(api): /v1/me/peer-pubkey only updates web-managed members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 409 not_web_member guard to POST /v1/me/peer-pubkey: the endpoint will only rewrite peer_pubkey on members that have dashboard_user_id set. CLI members own their on-disk keypair — overwriting their stored peer_pubkey would break the next WS hello because the signature verification would fail against the new pubkey. In practice this restriction is invisible to the legitimate browser flow: the dashboard always mints its apikey against the web member (dashboard_user_id is non-null by construction in mutations.ts). Guard ensures a misuse (e.g. a CLI-minted apikey being used to call peer-pubkey) gets a clear 409 instead of silently breaking the CLI's auth. Discovered during phase 3.5 smoke when a CLI-minted apikey clobbered the only openclaw member (CLI-owned) and the user's CLI signature would have stopped verifying on the next launch. --- packages/api/src/modules/mesh/v1-router.ts | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/api/src/modules/mesh/v1-router.ts b/packages/api/src/modules/mesh/v1-router.ts index 9856bac..08ba668 100644 --- a/packages/api/src/modules/mesh/v1-router.ts +++ b/packages/api/src/modules/mesh/v1-router.ts @@ -305,12 +305,33 @@ export const v1Router = new Hono() const body = c.req.valid("json"); const newPubkey = body.pubkey.toLowerCase(); const [existing] = await db - .select({ peerPubkey: meshMember.peerPubkey }) + .select({ + peerPubkey: meshMember.peerPubkey, + dashboardUserId: meshMember.dashboardUserId, + }) .from(meshMember) .where(eq(meshMember.id, key.issuedByMemberId)); if (!existing) { return c.json({ error: "member_not_found" }, 404); } + // Safety: only web-managed members (dashboardUserId set) can have + // their peer_pubkey rewritten via this endpoint. CLI-created + // members hold a real on-disk secret that matches their existing + // peer_pubkey; overwriting it would break their next WS hello + // (signature verification fails because the stored pubkey no + // longer matches the secret they sign with). The browser flow + // always mints its apikey against the dashboard member, so this + // restriction is invisible to legitimate callers. + if (!existing.dashboardUserId) { + return c.json( + { + error: "not_web_member", + detail: + "this endpoint only updates web-managed members (mesh.member.dashboard_user_id IS NOT NULL); CLI members own their on-disk keypair and can't have peer_pubkey rewritten remotely", + }, + 409, + ); + } const changed = existing.peerPubkey !== newPubkey; if (changed) { await db