Skip to content

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

Description

@michaeljstevens

Hi — first, thanks for the project, the concept is great.

While getting it running on macOS 26.3.1 with an MX Master 4 I hit four issues that I have local fixes for and would like to open a PR for, per the contributing guide.

Some overlap with #2 (where multiple users report "no haptics" on Tahoe even after the Smart Action is configured). Not related to #4 (notification watcher), which already has a separate fix proposed.

1. Browser extension is silently blocked by Chrome (CORS / PNA)

WebServer.swift returns a flat HTTP/1.1 200 OK with no CORS headers and doesn't handle the OPTIONS preflight. Modern Chrome:

  • Sends a preflight for any cross-origin POST from a chrome-extension:// origin to localhost.
  • Requires Access-Control-Allow-Private-Network: true (PNA, Chrome 130+) for any request to a private network.
  • Rejects Access-Control-Allow-Origin: * when credentials (cookies) are attached, which Chrome does for the chrome-extension → localhost case.

Net effect: extension popup shows "Connected" (because the GET succeeds), but no POST from the test buttons or content script ever reaches the server. Confirmed by sniffing the loopback — zero traffic. After adding preflight handling that echoes the request Origin and sets Allow-Credentials: true + Allow-Private-Network: true, the extension works.

2. @AppStorage defaults are never written to UserDefaults

ContentView declares:

@AppStorage("allowWebHaptics") private var allowWebHaptics: Bool = true
@AppStorage("systemNotificationsEnabled") private var systemNotificationsEnabled: Bool = true

But WebServer.processJson and NotificationWatcher read these via UserDefaults.standard.bool(forKey:), which returns false when the key is absent. @AppStorage doesn't actually write the default — it only writes when the binding is set.

Net effect: on a fresh install, both web haptics and notification haptics are silently off until the user toggles each switch in the UI. Easy fix: register defaults on launch via UserDefaults.standard.register(defaults:).

3. AppleScript keystroke path is unreliable on ad-hoc signed builds, macOS 14+

HapticEngine.trigger() uses NSAppleScripttell application "System Events" to key code 32 …, which requires the separate kTCCServiceAppleEvents Automation permission for "System Events" (in addition to Accessibility).

On macOS 14+ with ad-hoc signed builds (which build_with_icon.sh produces, since most users don't have a Developer ID), the Automation permission prompt is often suppressed due to cdhash validation failures (visible in log show as SecStaticCodeCheckValiditystatus: -67050). The app silently does nothing and the user has no UI feedback explaining why.

Replacing the AppleScript path with CGEvent.post(tap: .cghidEventTap):

  • Uses only the existing Accessibility permission, no Automation prompt needed.
  • Bypasses System Events entirely.
  • Is functionally indistinguishable to Logi Options+'s Smart Action listener — verified to fire on Tahoe 26.3.1.

This likely explains several "no haptics" reports in #2 where users had Accessibility granted and the Smart Action mapped correctly but still saw nothing.

4. Port 3000 collides with common dev servers

Hardcoded port 3000 collides with Next.js, Rails, Flask defaults, etc. When occupied, WebServer.start() swallows the bind error and the server silently fails to come up — the UI status stays at "Initializing…" forever.

Proposed fix:

  • Default port changed to 3030.
  • Port becomes configurable via the UI (added a TextField in the Web Browser card, persisted via @AppStorage("webServerPort")).
  • Bind failures now surface as "Port X in use" in the status row.

The browser extension (background.js/popup.js/manifest.json) currently hardcodes the port too — bumped its default to match. Making the extension port configurable as well could be a follow-up.

Bonus: build script not idempotent

mv build/"Haptic Master.app" . fails when a prior build exists, leaving the freshly built bundle in build/ and silently deploying the stale one (no error, since echo "✅ Build Complete!" runs unconditionally after). Trivial to fix by rm -rf the destination first or set -e in the script.

Note on rebuilds and cdhash

Worth documenting somewhere: each rebuild via build_with_icon.sh produces a new cdhash (ad-hoc signature changes with the binary). macOS appears to invalidate the prior Accessibility grant when the cdhash changes, requiring a re-grant via System Settings on every rebuild during development. Not something this PR addresses, but a footgun for any contributor.


I'll open a draft PR with fixes for (1) + (2) + (3) + (4) at once, since they're what makes the project work out of the box on a fresh macOS install. Happy to split, scope down, or change approach based on your guidance.

Tested locally on macOS 26.3.1 with MX Master 4 + Logi Options+ 2.x.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions