Skip to content

Fix CORS/PNA, write @AppStorage defaults, switch to CGEvent, make port configurable#6

Open
michaeljstevens wants to merge 3 commits into
weldonfisher:mainfrom
michaeljstevens:fix/cors-defaults-cgevent-port
Open

Fix CORS/PNA, write @AppStorage defaults, switch to CGEvent, make port configurable#6
michaeljstevens wants to merge 3 commits into
weldonfisher:mainfrom
michaeljstevens:fix/cors-defaults-cgevent-port

Conversation

@michaeljstevens

Copy link
Copy Markdown

Closes #5.

Tested locally on macOS 26.3.1 with MX Master 4 + Logi Options+. After these changes, on a fresh install: direct curl POSTs vibrate the mouse, browser extension test buttons vibrate the mouse, system notifications fire haptics, and the port can be changed live from the UI.

Summary

  • WebServer: handle OPTIONS preflight with Origin echo, Allow-Credentials, and Allow-Private-Network so the bundled browser extension can actually reach the server from modern Chrome. Port now read from UserDefaults (default 3030), supports restart() for live changes, and surfaces bind failures in status.
  • HapticMasterApp: register UserDefaults defaults on launch so non-SwiftUI reads see the same defaults the @AppStorage bindings advertise. Fixes silently-disabled web + notification haptics on fresh installs.
  • HapticEngine: replace NSAppleScript path with CGEvent.post. Removes the Automation→System Events permission requirement that's often suppressed on ad-hoc signed builds (macOS 14+) due to cdhash validation failures. Uses only Accessibility.
  • ContentView: port selection TextField in the Web Browser card. Validates 1024–65535, triggers WebServer.restart() on commit, with a note that the browser extension must use the same port.
  • build_with_icon.sh: set -e, and remove a stale "$APP_NAME.app" before building so the final mv doesn't silently fail and deploy stale artifacts.
  • extension / extension_firefox: bump hardcoded port 30003030 to match the new default.

Why these specific fixes

See #5 for the full diagnostic walkthrough — what was logged, what was sniffed, what TCC reported, etc.

Test plan

  • Direct curl -X POST http://localhost:3030/haptic -d '{"pattern":"single"}' vibrates the mouse (CGEvent path + Smart Action mapping).
  • Chrome extension "Test Hover" / "Test Click" buttons vibrate the mouse (CORS/PNA preflight succeeds).
  • Content script vibrates on link/button hover and click after page refresh.
  • Fresh install (with all UserDefaults cleared) has both haptics active by default.
  • Changing the port in the UI from 3030 → 3031 → 3030 restarts the listener correctly and status updates.
  • Port bind failure (running app twice, second instance) reports "Port X in use" instead of "Initializing…" forever.

Notes / known follow-ups

  • Browser extension still hardcodes the port. Making it configurable (via popup settings or polling the app for its current port) would round this out.
  • The cdhash-invalidates-Accessibility-on-rebuild footgun isn't addressed here — that's an ad-hoc signing limitation. Worth documenting in README for contributors, but out of scope for this PR.
  • Open to splitting this into multiple PRs if you'd prefer.

🤖 Generated with Claude Code

michaeljstevens and others added 3 commits June 4, 2026 17:22
…t configurable

Addresses weldonfisher#5.

WebServer: handle OPTIONS preflight with Origin echo,
Allow-Credentials, and Allow-Private-Network so the bundled browser
extension can actually reach the server from modern Chrome (PNA + chrome-extension
origin previously blocked every POST). Port is now read from UserDefaults,
defaults to 3030 (3000 collides with most dev servers), and supports restart()
for live changes.

HapticMasterApp: register UserDefaults defaults on launch so non-SwiftUI reads
(WebServer.processJson, NotificationWatcher) see the same defaults the
@AppStorage bindings advertise. Fresh installs previously had web haptics
and notification haptics silently disabled.

HapticEngine: replace NSAppleScript path with CGEvent.post. AppleScript
required a separate Automation permission for System Events that is often
suppressed on ad-hoc signed builds (macOS 14+) due to cdhash validation
failures; CGEvent uses only the already-granted Accessibility permission
and is functionally equivalent for Logi Options+'s Smart Action listener.

ContentView: port selection TextField in the Web Browser card. Persisted
via @AppStorage("webServerPort"), validates 1024-65535, triggers
WebServer.restart() on commit.

build_with_icon.sh: add set -e and remove a stale "$APP_NAME.app" before
building so the final mv doesn't silently fail and deploy stale artifacts.

extension / extension_firefox: bump hardcoded port from 3000 to 3030 to
match the new default. Configurable extension port is a possible follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reverts the default-port change to 3000 to preserve existing user
expectations (matches README and historical installs). Override
behavior is unchanged: change "webServerPort" via the Web Browser
card when 3000 is occupied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
popup gets a Server Port input that persists to chrome.storage.local
(default 3000). popup.js uses it for the connection probe; background.js
reads it from storage before each /haptic fetch.

manifest host_permissions changed from "http://localhost:3000/*" to
"http://localhost/*" — ports are not part of match patterns, so this
matches any localhost port without needing wildcards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@michaeljstevens

Copy link
Copy Markdown
Author

Added a follow-up commit (b6edc7b) that addresses the extension-side configurability I flagged as a follow-up in the PR description:

  • Popup now has a Server Port input that persists to chrome.storage.local (default 3000), drives the connection probe, and updates immediately on change.
  • background.js reads the port from storage before each /haptic fetch (chrome.storage handles the cross-context sync; the MV3 service worker doesn't need to cache).
  • manifest.json host_permissions is widened to "http://localhost/*" — ports aren't part of match patterns, so this matches any localhost port without needing wildcards or <all_urls>. Same in the Firefox manifest.

With this, changing the port in the app's UI now only requires opening the extension popup and entering the same port — no need to repackage the extension. Both Chrome and Firefox extensions kept in sync.

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.

Browser extension blocked by CORS/PNA, web+notification haptics off by default, AppleScript path unreliable on macOS 14+ — fixes ready

1 participant