voice: fail loud on insecure-context join (mic blocked over HTTP) instead of silent reject — TDD

getUserMedia is only exposed in a secure context (HTTPS, or the localhost
exemption). Reached over plain HTTP on a LAN IP — the dev server from a phone
at http://192.168.x.x:8000 — iOS/Android leave navigator.mediaDevices
undefined, so join() fetched TURN creds (a confusing 200) then silently
rejected deep in a .then() with no mic prompt. Desktop works because
127.0.0.1 IS a secure context. (Not a regression — voice never worked on the
HTTP dev server from mobile.)

- voice-mesh.js: _micSupported() seam + an early INSECURE_CONTEXT reject in
  join() before any network, so the failure is fast + diagnosable.
- burger-btn.js: bindVoiceBtn catches the rejected join, rolls back the
  optimistic .in-call/dataset.inCall (so the next click retries), and surfaces
  a Brief — 'Voice needs HTTPS (or localhost) — your browser blocked the mic
  here.' — instead of failing invisibly.
- VoiceMeshSpec: +2 specs (join rejects INSECURE_CONTEXT; btn rolls back +
  Briefs on reject). Jasmine green.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-29 21:29:42 -04:00
parent 6799749ede
commit da97c623c9
4 changed files with 169 additions and 1 deletions

View File

@@ -170,8 +170,27 @@
}
};
// getUserMedia is only exposed in a secure context — HTTPS, or the
// localhost/127.0.0.1 exemption. Over plain HTTP on a LAN IP (e.g. the dev
// server reached from a phone at http://192.168.x.x:8000) iOS/Android leave
// `navigator.mediaDevices` undefined, so the mic call would throw with no
// permission prompt. Pulled into a method so it's a clean test seam.
VoiceRoom.prototype._micSupported = function () {
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
};
VoiceRoom.prototype.join = function (roomId) {
var self = this;
// Fail fast + LOUD when the mic can't be reached — otherwise join would
// fetch TURN creds (a confusing 200) and then silently reject deep in a
// .then() with no prompt. Callers surface `err.code` to the user.
if (!this._micSupported()) {
var err = new Error(
'Voice needs a secure context (HTTPS, or localhost) — your ' +
'browser blocked the mic here. Open the HTTPS site to talk.');
err.code = 'INSECURE_CONTEXT';
return Promise.reject(err);
}
return this._fetchTurn().then(function (creds) {
if (creds && creds.iceServers) self.iceServers = creds.iceServers;
return navigator.mediaDevices.getUserMedia({