fix(rename): split 404 vs 403 + surface API error body (v1.19.2)
The rename route was collapsing "mesh doesn't exist" and "exists
but you don't own it" into a single 404 with body
{"error":"mesh not found or you are not the owner"}, and the CLI
was throwing that body away — the user only saw "API error 404:
Not Found", which is actively misleading when they have multiple
accounts and signed in to the wrong one.
Server: separate lookup-then-update. 404 only when the slug is
missing; 403 with an actionable message when the caller is not
the owner.
CLI: parse the {error} body off ApiError and print it instead of
the bare statusText. Map status codes to specific exit codes
(401 -> AUTH_FAILED, 403 -> PERMISSION_DENIED, 404 -> NOT_FOUND).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "1.19.1",
|
"version": "1.19.2",
|
||||||
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { rename as renameMesh } from "~/services/mesh/facade.js";
|
import { rename as renameMesh } from "~/services/mesh/facade.js";
|
||||||
import { getStoredToken } from "~/services/auth/facade.js";
|
import { getStoredToken } from "~/services/auth/facade.js";
|
||||||
|
import { ApiError } from "~/services/api/facade.js";
|
||||||
import { bold, dim, green, icons } from "~/ui/styles.js";
|
import { bold, dim, green, icons } from "~/ui/styles.js";
|
||||||
import { EXIT } from "~/constants/exit-codes.js";
|
import { EXIT } from "~/constants/exit-codes.js";
|
||||||
|
|
||||||
export async function rename(slug: string, newName: string): Promise<number> {
|
export async function rename(slug: string, newName: string): Promise<number> {
|
||||||
// Rename hits the account-scoped /api/my/meshes endpoint, which requires a
|
// Rename requires a web session token (~/.claudemesh/auth.json). Joining
|
||||||
// web session token (~/.claudemesh/auth.json). Joining a mesh via invite
|
// a mesh via invite does NOT create that token — it only writes a per-mesh
|
||||||
// does NOT create that token — it only writes a per-mesh apikey to
|
// apikey to config.json. Detect this case up front so the error is actionable.
|
||||||
// config.json. Detect this case up front so the error is actionable.
|
|
||||||
const auth = getStoredToken();
|
const auth = getStoredToken();
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
console.error(` ${icons.cross} Renaming a mesh requires a claudemesh.com account session.`);
|
console.error(` ${icons.cross} Renaming a mesh requires a claudemesh.com account session.`);
|
||||||
@@ -22,6 +22,18 @@ export async function rename(slug: string, newName: string): Promise<number> {
|
|||||||
console.log(` ${green(icons.check)} Renamed "${slug}" to "${newName}"`);
|
console.log(` ${green(icons.check)} Renamed "${slug}" to "${newName}"`);
|
||||||
return EXIT.SUCCESS;
|
return EXIT.SUCCESS;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
// Server returns { error: "..." } for actionable messages
|
||||||
|
// (not the owner, mesh missing, expired token). Surface the
|
||||||
|
// body — the bare HTTP code alone hides why it failed.
|
||||||
|
const body = err.body as { error?: string } | undefined;
|
||||||
|
const detail = body?.error ?? err.statusText;
|
||||||
|
console.error(` ${icons.cross} ${detail}`);
|
||||||
|
if (err.status === 401) return EXIT.AUTH_FAILED;
|
||||||
|
if (err.status === 403) return EXIT.PERMISSION_DENIED;
|
||||||
|
if (err.status === 404) return EXIT.NOT_FOUND;
|
||||||
|
return EXIT.INTERNAL_ERROR;
|
||||||
|
}
|
||||||
console.error(` ${icons.cross} Failed: ${err instanceof Error ? err.message : err}`);
|
console.error(` ${icons.cross} Failed: ${err instanceof Error ? err.message : err}`);
|
||||||
return EXIT.INTERNAL_ERROR;
|
return EXIT.INTERNAL_ERROR;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,19 +93,36 @@ export async function PATCH(
|
|||||||
return NextResponse.json({ error: "name too long (max 80 chars)" }, { status: 400 });
|
return NextResponse.json({ error: "name too long (max 80 chars)" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only the owner can rename. The slug is the public identifier; we
|
// Look up the mesh first so we can distinguish "doesn't exist"
|
||||||
// match (slug, ownerUserId) to scope the update.
|
// (404) from "exists but you don't own it" (403). The CLI was
|
||||||
const [updated] = await db
|
// collapsing both into a bare "API error 404" — unhelpful when
|
||||||
.update(mesh)
|
// the user has multiple accounts and signs in to the wrong one.
|
||||||
.set({ name: newName })
|
const [existing] = await db
|
||||||
.where(and(eq(mesh.slug, slug), eq(mesh.ownerUserId, payload.sub)))
|
.select({ ownerUserId: mesh.ownerUserId })
|
||||||
.returning({ slug: mesh.slug, name: mesh.name });
|
.from(mesh)
|
||||||
|
.where(eq(mesh.slug, slug))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!updated) {
|
if (!existing) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "mesh not found or you are not the owner" },
|
{ error: `mesh "${slug}" not found` },
|
||||||
{ status: 404 },
|
{ status: 404 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (existing.ownerUserId !== payload.sub) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: `you are signed in as a different account than the owner of "${slug}". Run \`claudemesh logout && claudemesh login\` and pick the owning account.`,
|
||||||
|
},
|
||||||
|
{ status: 403 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(mesh)
|
||||||
|
.set({ name: newName })
|
||||||
|
.where(eq(mesh.slug, slug))
|
||||||
|
.returning({ slug: mesh.slug, name: mesh.name });
|
||||||
|
|
||||||
return NextResponse.json(updated);
|
return NextResponse.json(updated);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user