Summary
What information should navigator.install() (and <install>'s InstallResultEvent) expose to the calling origin after an install attempt — particularly in the cross-origin case, where origin A is installing origin B's app.
Current behavior
Today, the calling origin receives 1 of 3 results:
| Result |
Meaning |
How it surfaces |
kSuccess |
Install accepted by user (or app was already installed and user chose to open it) |
navigator.install() resolves with a WebInstallResult containing the installed app's computed manifest id |
kDataError |
Manifest fetch failed, manifest invalid, no manifest_id found, manifest_id mismatch, manifest not installable |
navigator.install() rejects with DOMException("DataError") |
kAbortError |
Failure for any other reason, including user cancellation, permission denied, install already in progress, profile destruction, etc. |
navigator.install() rejects with DOMException("AbortError") |
Problems with current behavior
-
kAbortError is overloaded. Only 1 of ~10 cases mapped to kAbortError is actually "user cancelled." The rest are internal failures (permission denied, dialog already showing, etc.). This was raised by developers during trials as a usability limitation. Furthermore, it does not match the web platform convention where AbortError means the user cancelled the operation (see Payment Request API, File Picker API, Web Share API, Credential Management API).
-
kSuccess leaks cross-origin information. Privacy reviewer raised the concern that origin A could enumerate the cross-origin apps that were successfully installed.
-
manifest_id was previously returned on success. We have already agreed to stop exposing the computed manifest_id in the resolved result, as it would let a caller probe cross-origin app identity.
Proposed options
Three options, in order of decreasing information:
Option 1: Three distinct outcomes (current approach % minor tweaks)
AbortError rejection → user cancelled only (narrowed from catch-all)
DataError rejection → manifest/data issue (unchanged)
- Promise resolves → success (
manifest_id is dropped, otherwise unchanged)
Tradeoffs:
- ✅ Best developer experience —
DataError signals fixable issues, AbortError enables retry UX
- ✅ Origin Trial developer feedback confirmed "did the user cancel?" is the most-requested signal
- ✅ Aligns
AbortError with web platform convention (user-initiated cancellation only)
- ❌ Success is still exposed
- ❌ What happens to the catch-all failure cases that were dropped from
AbortError?
Option 2: Two distinct outcomes
AbortError rejection → user cancelled only
DataError rejection → manifest/data issue
- Everything else (success, internal failures) → promise resolves
Tradeoffs:
- ✅ Preserves the retry UX signal (
AbortError)
- ✅ Preserves developer diagnostics (
DataError)
- ✅ Success and internal failures are indistinguishable — reduces cross-origin leak
- ❌ A bit illogical to resolve the promise for internal failures
Option 3: One distinct outcome (maximum privacy)
DataError rejection → manifest/data issue
- Everything else (success, abort, internal failures) → promise resolves silently
Tradeoffs:
- ✅ Maximum privacy — caller only learns about fixable data errors
- ✅ Simplest API surface
- ❌ Eliminates the retry UX signal that OT developers explicitly requested
- ❌ Developers lose all ability to distinguish user cancellation from success
Side-channel considerations
Regardless of which option is chosen, the calling origin can partially infer outcomes through observable side channels:
- Focus/blur events reveal whether a browser-rendered dialog was shown
- Timing differences reveal which code path was taken (a fast rejection suggests a data error; a multi-second pause suggests user interaction with a dialog)
These side channels exist independently of the promise result and limit the incremental privacy benefit of withholding explicit results. This was acknowledged by a previous security reviewer.
Context from security/privacy review
From Nicolás Peña Moreno (June 2026):
"I think it would be better not to tell Origin A anything, if possible."
When asked whether distinguishing abort (user cancelled) from other failures is acceptable: "Yea that seems ok."
Nicolás also asked what happens when the app is already installed and the user (a) opens it or (b) dismisses the dialog — confirming this is a live concern for the already-installed case.
We have agreed to:
- Stop exposing the computed
manifest_id on successful install
- Keep
kDataError for developer usability (manifest id prerequisites make this critical)
Open sub-questions
- If we narrow
AbortError to user-cancelled-only, what happens to the other ~9 internal failure cases currently mapped to it? Options: silent resolve, new error type, or collapse into AbortError anyway.
- What result does the caller see when the app is already installed and the user (a) opens it or (b) dismisses the dialog?
- Should the
<install> element's InstallResultEvent use the same taxonomy, or can it differ? (Current position: same taxonomy, since they share a backend.)
Related
Summary
What information should
navigator.install()(and<install>'sInstallResultEvent) expose to the calling origin after an install attempt — particularly in the cross-origin case, where origin A is installing origin B's app.Current behavior
Today, the calling origin receives 1 of 3 results:
kSuccessnavigator.install()resolves with aWebInstallResultcontaining the installed app's computed manifest idkDataErrormanifest_idfound,manifest_idmismatch, manifest not installablenavigator.install()rejects withDOMException("DataError")kAbortErrornavigator.install()rejects withDOMException("AbortError")Problems with current behavior
kAbortErroris overloaded. Only 1 of ~10 cases mapped tokAbortErroris actually "user cancelled." The rest are internal failures (permission denied, dialog already showing, etc.). This was raised by developers during trials as a usability limitation. Furthermore, it does not match the web platform convention whereAbortErrormeans the user cancelled the operation (see Payment Request API, File Picker API, Web Share API, Credential Management API).kSuccessleaks cross-origin information. Privacy reviewer raised the concern that origin A could enumerate the cross-origin apps that were successfully installed.manifest_idwas previously returned on success. We have already agreed to stop exposing the computedmanifest_idin the resolved result, as it would let a caller probe cross-origin app identity.Proposed options
Three options, in order of decreasing information:
Option 1: Three distinct outcomes (current approach % minor tweaks)
AbortErrorrejection → user cancelled only (narrowed from catch-all)DataErrorrejection → manifest/data issue (unchanged)manifest_idis dropped, otherwise unchanged)Tradeoffs:
DataErrorsignals fixable issues,AbortErrorenables retry UXAbortErrorwith web platform convention (user-initiated cancellation only)AbortError?Option 2: Two distinct outcomes
AbortErrorrejection → user cancelled onlyDataErrorrejection → manifest/data issueTradeoffs:
AbortError)DataError)Option 3: One distinct outcome (maximum privacy)
DataErrorrejection → manifest/data issueTradeoffs:
Side-channel considerations
Regardless of which option is chosen, the calling origin can partially infer outcomes through observable side channels:
These side channels exist independently of the promise result and limit the incremental privacy benefit of withholding explicit results. This was acknowledged by a previous security reviewer.
Context from security/privacy review
From Nicolás Peña Moreno (June 2026):
We have agreed to:
manifest_idon successful installkDataErrorfor developer usability (manifest id prerequisites make this critical)Open sub-questions
AbortErrorto user-cancelled-only, what happens to the other ~9 internal failure cases currently mapped to it? Options: silent resolve, new error type, or collapse intoAbortErroranyway.<install>element'sInstallResultEventuse the same taxonomy, or can it differ? (Current position: same taxonomy, since they share a backend.)Related