Skip to content

fix: restore link capture on Windows and macOS - #3

Merged
rgdevment merged 2 commits into
mainfrom
feat/performance
Aug 4, 2026
Merged

fix: restore link capture on Windows and macOS#3
rgdevment merged 2 commits into
mainfrom
feat/performance

Conversation

@rgdevment

@rgdevment rgdevment commented Aug 4, 2026

Copy link
Copy Markdown
Owner

harden IPC and add source-app rules

Link capture was broken on both platforms, for unrelated reasons.

Windows: the shell registration was written once on first boot and never reconciled again, so a moved, updated or reinstalled executable left HKCU pointing at a path that no longer existed. Since HKCU\Software\Classes shadows HKLM, a leftover entry from a local build also hijacked link handling from the real installation — Store (MSIX) or standalone alike.

macOS: AppDelegate called super.applicationDidFinishLaunching, a selector FlutterAppDelegate does not implement. It compiles because the superclass adopts NSApplicationDelegate, but the objc_msgSendSuper hits an unimplemented selector and aborts the process during launch — precisely when Launch Services hands it a URL to open.

Registration

  • Reconcile the handler on every launch, before delegating to the resident instance, so a courier process repairs it too.
  • Never register from a build tree, and remove entries a build tree owns.
  • Under MSIX, drop per-user entries shadowing the package manifest.
  • Make "set as default" actually re-register instead of only opening system settings; point macOS at Desktop & Dock rather than the pre-Ventura pane.
  • Detect MSIX via GetCurrentPackageFullName: the previous environment-variable probe was inheritable, and a false positive disabled registration entirely.

macOS

  • Drop the unimplemented super call in applicationDidFinishLaunching.
  • Register the running bundle, not whatever Launch Services resolves, so a debug build cannot claim the association and die with flutter clean.
  • Report setDefaultApplication failures instead of discarding them.
  • Resolve the Flutter window through the delegate outlet; NSApp.windows.first could return the tray window.
  • LSHandlerRank Owner: as Alternate the app ruled itself out as a browser.
  • Include Safari's cryptex path in browser detection.
  • Widen the login-item catch: MissingPluginException killed the launch.

Windows IPC

  • Give the pipe an explicit descriptor: DACL limited to the current user and SYSTEM, low-integrity label so medium-integrity callers can still deliver.
  • Reject remote clients and require first-instance ownership, and use SECURITY_IDENTIFICATION so a squatter cannot impersonate the client.
  • Launch the app unelevated after install (runasoriginaluser): inheriting the installer's token put the mutex and pipe out of reach of Slack and Teams.
  • Buffer the argv URL through the pipe server instead of an async* wrapper that dropped events flushed before subscription.
  • Log unrecognised launch arguments; dropped links left no trace at all.

Window and rendering

  • Sequence window setup instead of relying on waitUntilReadyToShow's callback, which is not awaited.
  • Serialise mode transitions and drive the window from one place only; two concurrent paths could leave it visible with no content.
  • Arm the picker blur guard by timer only. Arming it on first focus meant the focus event from showing the window closed the picker immediately.
  • Reconcile window visibility from the hidden state as a safety net.
  • Re-scan browsers after resetting a corrupt config, and show an explanation in the picker instead of a blank window.
  • Clamp picker position safely; on small or scaled displays it threw and the app went deaf to every later link.

Security

  • Reject non-launchable URLs before any Process.start: a crafted --gpu-launcher= reached the browser as argv and ran arbitrary binaries.
  • Match the file scheme case-insensitively; FILE:// bypassed the extension allowlist. Reject UNC paths, which leaked a NetNTLMv2 hash on probe.
  • Redact errors and stack traces, and cap startup_crash.log: a failed launch wrote the full URL to disk, contradicting PRIVACY.md.
  • Validate the release URL as github.com before handing it to the shell.
  • Surface malformed IPC payloads as FormatException instead of TypeError.

Features

  • Capture the microsoft-edge: protocol (opt-in). Teams, Outlook, Widgets and Start search wrap links in it, bypassing the default browser entirely.
  • Private windows from the picker with Shift, using the switch each browser family expects. On macOS this needs open -n, or --args is discarded.
  • Rules scoped to the originating app, resolved from the parent process on Windows and approximated by the frontmost app on macOS. App-scoped rules take precedence over domain rules.
  • Self-diagnostics in Settings that explain a broken registration and offer a one-click repair.

Build reproducibility

  • Track pubspec.lock and pin the Flutter version in CI and release workflows; every release was previously built against whatever stable resolved to.

Docs: add docs/LINK_CAPTURE.md covering the capture path, registry precedence, the log messages that identify each failure, and the new features.

…urce-app rules

Link capture was broken on both platforms, for unrelated reasons.

Windows: the shell registration was written once on first boot and never
reconciled again, so a moved, updated or reinstalled executable left HKCU
pointing at a path that no longer existed. Since HKCU\Software\Classes shadows
HKLM, a leftover entry from a local build also hijacked link handling from the
real installation — Store (MSIX) or standalone alike.

macOS: AppDelegate called super.applicationDidFinishLaunching, a selector
FlutterAppDelegate does not implement. It compiles because the superclass
adopts NSApplicationDelegate, but the objc_msgSendSuper hits an unimplemented
selector and aborts the process during launch — precisely when Launch Services
hands it a URL to open.

Registration
- Reconcile the handler on every launch, before delegating to the resident
  instance, so a courier process repairs it too.
- Never register from a build tree, and remove entries a build tree owns.
- Under MSIX, drop per-user entries shadowing the package manifest.
- Make "set as default" actually re-register instead of only opening system
  settings; point macOS at Desktop & Dock rather than the pre-Ventura pane.
- Detect MSIX via GetCurrentPackageFullName: the previous environment-variable
  probe was inheritable, and a false positive disabled registration entirely.

macOS
- Drop the unimplemented super call in applicationDidFinishLaunching.
- Register the running bundle, not whatever Launch Services resolves, so a
  debug build cannot claim the association and die with `flutter clean`.
- Report setDefaultApplication failures instead of discarding them.
- Resolve the Flutter window through the delegate outlet; NSApp.windows.first
  could return the tray window.
- LSHandlerRank Owner: as Alternate the app ruled itself out as a browser.
- Include Safari's cryptex path in browser detection.
- Widen the login-item catch: MissingPluginException killed the launch.

Windows IPC
- Give the pipe an explicit descriptor: DACL limited to the current user and
  SYSTEM, low-integrity label so medium-integrity callers can still deliver.
- Reject remote clients and require first-instance ownership, and use
  SECURITY_IDENTIFICATION so a squatter cannot impersonate the client.
- Launch the app unelevated after install (runasoriginaluser): inheriting the
  installer's token put the mutex and pipe out of reach of Slack and Teams.
- Buffer the argv URL through the pipe server instead of an async* wrapper
  that dropped events flushed before subscription.
- Log unrecognised launch arguments; dropped links left no trace at all.

Window and rendering
- Sequence window setup instead of relying on waitUntilReadyToShow's callback,
  which is not awaited.
- Serialise mode transitions and drive the window from one place only; two
  concurrent paths could leave it visible with no content.
- Arm the picker blur guard by timer only. Arming it on first focus meant the
  focus event from showing the window closed the picker immediately.
- Reconcile window visibility from the hidden state as a safety net.
- Re-scan browsers after resetting a corrupt config, and show an explanation
  in the picker instead of a blank window.
- Clamp picker position safely; on small or scaled displays it threw and the
  app went deaf to every later link.

Security
- Reject non-launchable URLs before any Process.start: a crafted
  `--gpu-launcher=` reached the browser as argv and ran arbitrary binaries.
- Match the file scheme case-insensitively; `FILE://` bypassed the extension
  allowlist. Reject UNC paths, which leaked a NetNTLMv2 hash on probe.
- Redact errors and stack traces, and cap startup_crash.log: a failed launch
  wrote the full URL to disk, contradicting PRIVACY.md.
- Validate the release URL as github.com before handing it to the shell.
- Surface malformed IPC payloads as FormatException instead of TypeError.

Features
- Capture the microsoft-edge: protocol (opt-in). Teams, Outlook, Widgets and
  Start search wrap links in it, bypassing the default browser entirely.
- Private windows from the picker with Shift, using the switch each browser
  family expects. On macOS this needs `open -n`, or --args is discarded.
- Rules scoped to the originating app, resolved from the parent process on
  Windows and approximated by the frontmost app on macOS. App-scoped rules
  take precedence over domain rules.
- Self-diagnostics in Settings that explain a broken registration and offer a
  one-click repair.

Build reproducibility
- Track pubspec.lock and pin the Flutter version in CI and release workflows;
  every release was previously built against whatever stable resolved to.

Docs: add docs/LINK_CAPTURE.md covering the capture path, registry precedence,
the log messages that identify each failure, and the new features.
@rgdevment rgdevment changed the title fix: restore link capture on Windows and macOS, harden IPC and add so… fix: restore link capture on Windows and macOS Aug 4, 2026
@rgdevment
rgdevment merged commit 01d39b8 into main Aug 4, 2026
7 checks passed
@rgdevment
rgdevment deleted the feat/performance branch August 4, 2026 22:20
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.

1 participant