fix(widget): don't offer a mic the site's policy forbids - #474
Merged
Conversation
Reported: "Microphone permission denied" on both FleetCrown and OrangeCat.
Measured in the browser rather than guessed:
orangecat.ch featurePolicyAllowsMic: false
permissionState: "denied" (not "prompt")
getUserMedia -> NotAllowedError
fleetcrown featurePolicyAllowsMic: true
permissionState: "prompt" (works)
Two different situations behind one error message. OrangeCat sends
`permissions-policy: camera=(), microphone=(), geolocation=()` from
next.config.js. `microphone=()` is an EMPTY allowlist — denied for every
origin including the site itself — so the browser never shows a prompt at
all. There is nothing the visitor can allow.
The bug is ours: navigator.mediaDevices.getUserMedia still EXISTS in that
state, so isVoiceSupported() passed and we drew a "Speak" button that could
only ever fail, then blamed the visitor's permissions for it.
isVoiceSupported now consults document.featurePolicy, so a policy-blocked
site simply never sees the button — the same progressive enhancement the
function already applied to a missing MediaRecorder or an insecure origin.
An unknown answer means ALLOWED, deliberately: featurePolicy is
non-standard and absent in some browsers, and hiding a working mic there
would be a worse failure than a click that fails with a readable message.
This is the half that is centrally fixable — one bundle, every customer
site, including sites we will never see. The other half is not, and cannot
be: Permissions-Policy is the host document's decision by design and no
script can override it. So the customer-facing docs now say plainly that
the mic needs `microphone=(self)`, what the symptom looks like, and that
everything else in the widget works regardless.
Pinned by four assertions in scripts/test/widget-voice.ts covering denied,
allowed, API-absent and API-throwing. Proven by mutation: making the check
always return true turns exactly the "denies the microphone" assertion red.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported: "Microphone permission denied" on both FleetCrown and OrangeCat. Measured in the browser rather than guessed:
promptdeniedNotAllowedErrorTwo different situations behind one error message. OrangeCat sends
permissions-policy: camera=(), microphone=(), geolocation=().microphone=()is an EMPTY allowlist — denied for every origin including the site itself — so the browser never shows a prompt. There is nothing the visitor can allow.The bug is ours
navigator.mediaDevices.getUserMediastill exists in that state. SoisVoiceSupported()passed, we drew a "Speak" button that could only ever fail, and then reported "Microphone permission denied" — pointing at the visitor rather than at the header actually responsible.isVoiceSupported()now consultsdocument.featurePolicy, so a policy-blocked site never sees the button. Same progressive enhancement already applied to a missingMediaRecorderor an insecure origin: a control that cannot work is worse than no control, because the visitor spends effort discovering that.Unknown means allowed, deliberately —
featurePolicyis non-standard and absent in some browsers, and hiding a working mic would be worse than a click that fails with a readable message.What is and is not centrally fixable
This is the half that is: one bundle, every customer site, including ones we will never see.
Enabling the mic is not, and cannot be —
Permissions-Policyis the host document's decision by web-platform design; no script can override it. So the customer docs now state the symptom, the cause, and the one-line fix (microphone=(self)), and note everything else works regardless.OrangeCat's own header is fixed in bitbaum/orangecat#(companion PR).
Verification
Four assertions in
scripts/test/widget-voice.ts: denied / allowed / API-absent / API-throwing. Proven by mutation — forcing the check to returntrueturns exactly the "denies the microphone" assertion red.npm run verifyexits 0. Bundle rebuilt (25,957 bytes).