web: serve COOP/COEP cross-origin isolation headers (fix audio in iOS WKWebView / MIDIWeb)#1022
Conversation
iOS WKWebView (e.g. the MIDIWeb app) does not honor the mini-coi.js service-worker shim that Tulip Web and AMYboard Web use to become cross-origin isolated, so SharedArrayBuffer / AudioWorklet / WASM never initialize there and the apps produce no audio (works in iOS Safari). Serve the COOP/COEP/CORP headers directly from Vercel instead, scoped to the app routes (/run, /editor) so the marketing landing pages keep loading their cross-origin YouTube embed + newsletter script (COEP would block third-party resources that lack CORP). Also copy tulip/web/vercel.json into stage/ at build time: the release/preview workflows run `vercel deploy stage`, so the file was previously ignored and its headers (incl. the existing cache rules) were never served. amyboardweb's dev.py already does the equivalent copy. Refs 5of12/MIDIWeb-Hub#23 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🔌 AMYboard PR previewEditor + flasher: https://amyboard-pr-1022.vercel.app/editor/ This preview bundles this PR's firmware — its flasher only flashes this build (not the release). Rebuilt on every push; removed when the PR closes. The hardware CI has been kicked off and should return within a few minutes, stand by! |
The /run catch-all cache rule used "/((?!run/(amy|tulipcc)-).*)", whose
nested capturing group inside the negative lookahead is rejected by
Vercel's path-to-regexp ("Capturing groups are not allowed"). It never
surfaced before because tulip/web/vercel.json was never deployed; the
previous commit (copying it into stage/) made Vercel parse it for the
first time, failing the deploy.
Use bare alternation "/((?!run/amy-|run/tulipcc-).*)" instead, mirroring
amyboardweb's working "/((?!amy-|amyboard-).*)". Validated with
path-to-regexp@6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🌷 Tulip Web PR previewTulip Web: https://tulip-pr-1022.vercel.app/run/ Flash a Tulip to this build: on-device run Rebuilt on every push; removed when the PR closes. |
Verified on the Vercel previews ✅Both previews build & deploy cleanly, serve the isolation headers on the app routes only, and the apps come up cross-origin isolated with no COEP-blocked resources. Checked with
Remaining: on-device verification in MIDIWeb (iOS WKWebView), which CI can't do. The preview URLs are already isolated, so loading |
With COOP/COEP now served by the server (Vercel via vercel.json; webdev.py for local dev), the mini-coi.js shim is redundant wherever the app is actually served. Left active it still registers a service worker on first visit and fires a location.reload() via its updatefound handler — an unnecessary full reload on every fresh load. Bail out when self.crossOriginIsolated is already true, keeping the shim only as a fallback for header-less static hosts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7a092c7 to
456cebb
Compare
…ut the slash Tulip Web's /run page references its assets relatively (amyrepl.css, tree.js, the WASM/mjs, etc.), which only resolve when the document path ends in /run/. Loading /run without a trailing slash resolved every asset against / and 404'd them, so loadMicroPython/start_audio were undefined and the app showed a blank page / stray textarea. iOS hit this because the URL had no trailing slash; desktop used the /run/ link and was fine. Add <base href="/run/"> so assets resolve correctly whether the page loads as /run or /run/. This mirrors amyboardweb/editor's existing <base href="/"> (which is why the editor already works at both /editor and /editor/), so no redirect is needed. Pre-existing on prod too — surfaced now that we point people at /run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
456cebb to
bc8d847
Compare
🎛️ HW CI (physical bench)AMYboard (USB-MIDI + AMY Tulip (TULIP4_R11; serial-REPL audio + WiFi screenshot): ✅ PASS — flashed this PR’s firmware; all checks matched the references. ⬇️ Artifacts: recordings · screenshot · serial logs · run logs Self-hosted bench. Audio spectral-compared to |
✅ Confirmed working on a real iOS deviceTwo distinct bugs were in play
Also: Verification (headless Chrome on the previews)
Marketing landing pages ( |
…firmware flasher PR shorepine#1022 moved cross-origin isolation from the root-scoped mini-coi service worker to per-path Vercel headers scoped to /editor only. The editor embeds the firmware flasher with <iframe src="esptool/index.html"> and uses <base href="/">, so that resolves to /esptool/index.html — which then no longer received COEP. A require-corp page refuses to embed a non-isolated iframe, so the Upgrade Firmware tab rendered as "www.amyboard.com refused to connect". Apply the same COOP/COEP/CORP headers to /esptool and /esptool/(.*) so the flasher page is cross-origin isolated and embeddable again. This matches local dev, where webdev.py already sends these headers for every path (which is why the bug was prod/preview-only). esptool's only cross-origin dependency (crypto-js from jsDelivr) already returns Cross-Origin-Resource-Policy: cross-origin, so it still loads under COEP — no esptool changes needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
Per 5of12/MIDIWeb-Hub#23: Tulip Web (
/run) and the AMYboard editor (/editor) produce no audio inside iOS WKWebView (e.g. the MIDIWeb app), even though they work in iOS Safari.Root cause is cross-origin isolation. Both apps need
crossOriginIsolated === true(forSharedArrayBuffer+ AudioWorklet + WASM), which requires the document to be served with:Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corpToday those headers are injected client-side by the
mini-coi.jsservice-worker shim. Safari honors it; iOS WKWebView does not, so isolation never happens and audio is silent.Fix
Serve the COOP/COEP/CORP headers directly from Vercel via
vercel.json, so isolation no longer depends on a service worker.Scoped to the app routes (
/run,/editor) rather than/(.*). The marketing landing pages embed a cross-origin YouTube iframe and a createsend newsletter script — under a global COEPrequire-corp, the newsletter script (no CORP header) would be blocked. Tulip's landing page has no COEP today, so a global rule would be a regression. Scoping targets exactly the pages that need isolation and leaves the marketing pages untouched.Verified the app routes are COEP-safe: every cross-origin CDN dependency (codemirror, bootstrap, webmidi, jquery from cdnjs / jsdelivr / googleapis) already returns
Cross-Origin-Resource-Policy: cross-origin, and all same-origin WASM/JS passes COEP inherently. This is the same COEP policy the apps already run under in Safari viamini-coi, so resource loading is unchanged.mini-coi.jsis left in place as a harmless fallback for non-Vercel/local serving (it sets identical headers).Also:
tulip/web/vercel.jsonwas never deployedThe release/preview workflows run
vercel deploy stage, buttulip/web/build.shnever copiedvercel.jsonintostage/— so the file was silently ignored in production (its existing cache rules weren't applied either; hashed assets were served with Vercel's defaultmax-age=0instead ofimmutable). Added thecp vercel.json stage/thatamyboardweb/dev.pyalready does.How to test (preview deploys)
https://tulip-pr-<N>.vercel.app/run/https://amyboard-pr-<N>.vercel.app/editor/On each, in the console:
…and confirm audio (Tulip: bleep on first interaction; AMYboard: Simulate → tap a key). Marketing landing pages (
/) should be unchanged (no COEP header; YouTube embed still loads).Final verification of the actual MIDIWeb/WKWebView case needs an iOS device.
🤖 Generated with Claude Code