fix(api): /v1/me/peer-pubkey only updates web-managed members
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

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.
This commit is contained in:
Alejandro Gutiérrez
2026-05-02 23:08:50 +01:00
parent 95b16a23fc
commit 2e57173ed9

View File

@@ -305,12 +305,33 @@ export const v1Router = new Hono<Env>()
const body = c.req.valid("json"); const body = c.req.valid("json");
const newPubkey = body.pubkey.toLowerCase(); const newPubkey = body.pubkey.toLowerCase();
const [existing] = await db const [existing] = await db
.select({ peerPubkey: meshMember.peerPubkey }) .select({
peerPubkey: meshMember.peerPubkey,
dashboardUserId: meshMember.dashboardUserId,
})
.from(meshMember) .from(meshMember)
.where(eq(meshMember.id, key.issuedByMemberId)); .where(eq(meshMember.id, key.issuedByMemberId));
if (!existing) { if (!existing) {
return c.json({ error: "member_not_found" }, 404); 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; const changed = existing.peerPubkey !== newPubkey;
if (changed) { if (changed) {
await db await db