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

  1. Refresh the token list (`listProMcpTokens`) after any revoke or issue operation before showing actions on rows.
  2. On `err.data === "NOT_FOUND"`, treat as already-gone (idempotent success) and remove the row from the client UI.
  3. Verify the tokenId comes from the current user's own list, not a cached/global list.
  4. 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

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.