Skip to content

fix(widget): don't offer a mic the site's policy forbids - #474

Merged
catomean merged 1 commit into
mainfrom
fix/mic-policy
Sep 4, 2026
Merged

fix(widget): don't offer a mic the site's policy forbids#474
catomean merged 1 commit into
mainfrom
fix/mic-policy

Conversation

@catomean

@catomean catomean commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Reported: "Microphone permission denied" on both FleetCrown and OrangeCat. Measured in the browser rather than guessed:

featurePolicy permission getUserMedia
fleetcrown.orangecat.ch allows prompt works
orangecat.ch blocks denied NotAllowedError

Two 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.getUserMedia still exists in that state. So isVoiceSupported() 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 consults document.featurePolicy, so a policy-blocked site never sees the button. Same progressive enhancement already applied to a missing MediaRecorder or an insecure origin: a control that cannot work is worse than no control, because the visitor spends effort discovering that.

Unknown means allowed, deliberately — featurePolicy is 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-Policy is 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 return true turns exactly the "denies the microphone" assertion red.

npm run verify exits 0. Bundle rebuilt (25,957 bytes).

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.
@catomean
catomean merged commit 39aae9a into main Sep 4, 2026
3 checks passed
@catomean
catomean deleted the fix/mic-policy branch September 4, 2026 00:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant