koala73/worldmonitor · error · ConvexError
NOT_FOUND
NOT_FOUND
Error message
NOT_FOUND
What it means
Thrown by `revokeProMcpToken` when the token row is missing OR does not belong to the calling user. This is a tenancy gate: non-owner attempts surface as `NOT_FOUND` (not "forbidden") so the API doesn't leak the existence of other users' tokens — mirroring `apiKeys.revokeApiKey`. Plain-string ConvexError; `err.data === "NOT_FOUND"`. Occurs after `requireUserId` (so the caller IS authenticated) but the token id is wrong or foreign.
Source
Thrown at convex/mcpProTokens.ts:237
}));
},
});
/**
* Revoke a Pro MCP token row owned by the current user.
*
* Tenancy gate: the caller must own the row. Non-owner attempts surface
* as `NOT_FOUND` (don't leak existence of other users' tokens). Mirrors
* `apiKeys.revokeApiKey`.
*/
export const revokeProMcpToken = mutation({
args: { tokenId: v.id("mcpProTokens") },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const row = await ctx.db.get(args.tokenId);
if (!row || row.userId !== userId) {
throw new ConvexError("NOT_FOUND");
}
if (row.revokedAt) {
throw new ConvexError("ALREADY_REVOKED");
}
await ctx.db.patch(args.tokenId, { revokedAt: Date.now() });
return { ok: true };
},
});
View on GitHub (pinned to ffec79ac33)
Solutions
- Refresh the token list (`listProMcpTokens`) after any revoke or issue operation before showing actions on rows.
- On `err.data === "NOT_FOUND"`, treat as already-gone (idempotent success) and remove the row from the client UI.
- Verify the tokenId comes from the current user's own list, not a cached/global list.
- Do not retry with the same id — NOT_FOUND is deterministic for a given id+user.
Example fix
// before
await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: row.id });
// after — treat NOT_FOUND as idempotent removal
try {
await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: row.id });
} catch (err) {
if (err.data === "NOT_FOUND") {
// token already gone — remove from UI silently
} else throw err;
}
setTokens((t) => t.filter((x) => x.id !== row.id)); Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure the tokenId comes from the current user's own list before revoking
const mine = await convex.query(api.mcpProTokens.listProMcpTokens);
if (!mine.some((t) => t._id === tokenId)) { removeRow(tokenId); return; } Type guard
function ownsToken(row: { userId: string } | null, userId: string): boolean {
return !!row && row.userId === userId;
} Try / catch
try {
await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId });
} catch (err) {
if (err.data === "NOT_FOUND") removeRow(tokenId); // idempotent — already gone
else throw err;
} Prevention
- Refresh the token list after any issue/revoke before showing actions.
- Treat NOT_FOUND as idempotent success (token already gone).
- NOT_FOUND deliberately hides foreign tokens — don't retry with the same id.
- Verify tokenId comes from the current user's list.
When it happens
Trigger: Calling `revokeProMcpToken` with a tokenId that was already deleted, a typo'd/guessed id, or a token belonging to a different user. Also a stale client holding a tokenId from a deleted account, or a token id from a different Convex deployment.
Common situations: The client's token list is stale (a token was rotated/revoked by the cap logic in `issueProMcpToken` and the list wasn't refreshed); a user has multiple sessions and one revokes a token another is acting on; a cross-account bug passes the wrong userId's token id.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/cdc76c37a50cd13e.
Report an issue: GitHub.