Skip to content

MIDI Out: optional TLS so Web MIDI works from non-localhost origins - #32

Merged
tsondo merged 1 commit into
mainfrom
claude/midi-out-secure-context
Aug 30, 2026
Merged

tsondo merged 1 commit into
mainfrom
claude/midi-out-secure-context

Conversation

@tsondo

@tsondo tsondo commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Implements docs/MIDI_OUT_SECURE_CONTEXT_SPEC.md (added in this PR).

Problem

Web MIDI is gated behind window.isSecureContext. StemForge binds to 0.0.0.0, so a browser on a second machine reaches http://<server-ip>:8765 — an insecure origin, where the browser reports the MIDI permission as immovably "Block". In the 2026-08-30 hardware session that presentation cost ~20 minutes chasing Brave settings before the origin turned out to be the cause, and the old banner text ("Open StemForge at http://localhost:8765") named the one remedy a remote user cannot follow.

Changes

  • run.py: --ssl-certfile / --ssl-keyfile, mirroring uvicorn's parameter names, passed straight through to uvicorn.run (None/None is uvicorn's default — plain-HTTP behavior is byte-identical). Supplying one without the other is a startup error, exit 1, checked before the GPU lock so a config error can't leave a lock file behind. The banner prints https:// under TLS.
  • frontend/components/midi.js (the only frontend change): the secure-context banner no longer hardcodes port 8765 and no longer names loopback as the only fix. The isSecureContext guard itself is untouched.
  • README: new "MIDI Out from another machine" subsection covering the three deployment paths in order of preference — Tailscale serve, SSH tunnel, built-in TLS with mkcert — noting that a bare self-signed cert is not sufficient in Chrome/Brave and that HF Spaces already serves HTTPS. The MIDI section's "secure context" sentence now links to it.
  • CLAUDE.md: launcher and MIDI Out entries reference the new flags and spec.
  • tests/test_cli_args.py: flag defaults, storage, and the pair-check predicate in both directions (tested directly rather than via main(), which acquires the GPU lock). Wired into CI.

Per the spec's DO-NOT list: no cert generation, no --insecure escape hatch, no bind-host change, no new dependencies, no changes to backend/main.py, /api/midi/events, webmidi.js, or scheduler logic.

Validation

  • 5 new tests pass; run.py compiles; midi.js passes node --check; ci.yml parses
  • Lone-flag start verified: exits 1 with actionable error, no gpu.lock left behind
  • Banner verified: http://localhost:8765 with no flags, https://localhost:<port> with them
  • grep -rn "ws://\|WebSocket\|EventSource" frontend/ returns nothing (DoD [A11Y] [High] Transport bar controls missing accessible names #8)
  • The second-machine acceptance test (mkcert CA + hardware synth enumeration) needs real hardware and remains manual per spec §6.2

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vb2foWspag8yfjz1hvxas2


Generated by Claude Code

Implements docs/MIDI_OUT_SECURE_CONTEXT_SPEC.md (added here). Web MIDI is
gated behind window.isSecureContext, and StemForge binds to 0.0.0.0 — so a
browser on a second machine reaches http://<server-ip>:8765, an insecure
origin where the MIDI permission reads as immovably "Block". In the
2026-08-30 hardware session that presentation cost ~20 minutes of chasing
Brave settings before the origin turned out to be the cause, and the old
banner text ("Open StemForge at http://localhost:8765") pointed at the one
remedy a remote user cannot follow.

run.py grows --ssl-certfile/--ssl-keyfile, mirroring uvicorn's parameter
names and passed straight through to uvicorn.run (None/None is uvicorn's
default, so plain-HTTP behavior is byte-identical). Supplying one flag
without the other is a startup error, exit 1 — a lone flag means the user
wants TLS, and silently starting over HTTP would reproduce exactly the
confusion this exists to remove. The check runs before the GPU lock is
acquired so a config error can't leave a lock file behind. The banner
takes a scheme parameter and prints https:// when TLS is on; the hostname
stays localhost since the banner describes reaching the server from its
own machine.

The midi.js secure-context banner no longer hardcodes port 8765 and no
longer names loopback as the only fix — it now points at localhost on the
server's actual port or HTTPS, with a pointer to the README. This is the
only frontend change; the isSecureContext guard itself is untouched (it is
the browser's contract), and frontend/ still contains no ws://, WebSocket,
or EventSource usage, so TLS cannot introduce mixed-content failures.

README documents the three deployment paths in order of preference —
Tailscale serve (publicly trusted cert, works from mobile), SSH tunnel
(the origin genuinely is loopback, no certs at all), and the new flags
with an mkcert-generated cert — noting that a bare self-signed cert is
not sufficient in Chrome/Brave (the interstitial click-through does not
create a secure context) and that HF Spaces already serves HTTPS.
StemForge deliberately consumes cert paths and never generates or manages
certificates.

tests/test_cli_args.py covers the flag surface: defaults, storage, and
the pair-check predicate in both directions (tested directly rather than
through main(), which acquires the GPU lock). Wired into the CI test job.

Verified locally: lone-flag start exits 1 with the error text and leaves
no gpu.lock; the banner prints http://localhost:8765 with no flags and
https:// with them. The full second-machine acceptance test (mkcert CA +
hardware synth) needs real hardware and remains manual per the spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vb2foWspag8yfjz1hvxas2
@tsondo
tsondo merged commit c06c0b0 into main Aug 30, 2026
3 checks passed
tsondo pushed a commit that referenced this pull request Aug 30, 2026
Both branches appended test files to the same pytest invocation in
ci.yml; the resolution keeps all three additions. CLAUDE.md and README
auto-merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vb2foWspag8yfjz1hvxas2
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.

2 participants