-
Notifications
You must be signed in to change notification settings - Fork 101
Open
Description
Problem
The clawdhub delete <slug> command returns Unauthorized even when logged in as the skill owner.
Reproduction
clawdhub whoami
# ✔ sergical
clawdhub delete reflect-notes --yes
# ✖ UnauthorizedRoot Cause
In convex/httpApiV1.ts line 471-489, the delete handler catches all errors and returns 401:
async function skillsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) {
// ...
try {
const { userId } = await requireApiTokenUser(ctx, request)
await ctx.runMutation(internal.skills.setSkillSoftDeletedInternal, {
userId,
slug,
deleted: true,
})
return json({ ok: true }, 200, rate.headers)
} catch {
return text('Unauthorized', 401, rate.headers) // <-- catches everything
}
}The mutation setSkillSoftDeletedInternal can throw several errors:
User not foundSlug requiredSkill not found- Role assertion failure (for non-owners)
All of these currently surface as Unauthorized, which is misleading.
Suggested Fix
Differentiate between auth errors and other errors:
async function skillsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) {
const rate = await applyRateLimit(ctx, request, 'write')
if (!rate.ok) return rate.response
const segments = getPathSegments(request, '/api/v1/skills/')
if (segments.length !== 1) return text('Not found', 404, rate.headers)
const slug = segments[0]?.trim().toLowerCase() ?? ''
let userId: Id<'users'>
try {
const auth = await requireApiTokenUser(ctx, request)
userId = auth.userId
} catch {
return text('Unauthorized', 401, rate.headers)
}
try {
await ctx.runMutation(internal.skills.setSkillSoftDeletedInternal, {
userId,
slug,
deleted: true,
})
return json({ ok: true }, 200, rate.headers)
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'
if (message.includes('not found')) {
return text('Not found', 404, rate.headers)
}
if (message.includes('Only the owner') || message.includes('assertRole')) {
return text('Forbidden', 403, rate.headers)
}
return text(message, 400, rate.headers)
}
}This pattern should probably be applied to other handlers that have the same issue (undelete, publish, etc.).
Metadata
Metadata
Assignees
Labels
No labels