After some more discussion with NJ, we decided on this course of action:

* cancelDownload() no longer returns anything.
* If a cancelDownload() call succeeds, the original installation Promise is rejected (asynchronously) with an error code of CANCELED.
* If a cancelDownload() call fails (e.g. because the download had already finished), the installation continues on and the installation Promise is resolved or rejected as if cancelDownload() had never been called.
* Between clicking Cancel and getting a result back on the installation Promise, the UI sits in a new "Cancelling..." state (STATE_CANCELING_INSTALL) with the Cancel grayed out.
* Since bugs could cause the Promise to _never_ finish, there's a timeout that moves from this state to one where Close is allowed (STATE_CANCELING_HUNG).

I've implemented this and it feels pretty reasonable.