|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 5 | +HANDLER_VERIFIER="$SCRIPT_DIR/verify_macos_url_handler.swift" |
| 6 | +LSREGISTER_PATH="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" |
| 7 | +USER_APPLICATIONS_DIR="${HOME}/Applications" |
| 8 | +WORK_PREFIX="${TMPDIR:-/tmp}/aidictation-url-handler-contract." |
| 9 | +INSTALL_PREFIX="$USER_APPLICATIONS_DIR/AIDictationURLHandlerContract." |
| 10 | +WORK_DIR="" |
| 11 | +INSTALL_ROOT="" |
| 12 | +INSTALLED_APP="" |
| 13 | + |
| 14 | +cleanup() { |
| 15 | + if [[ -n "$INSTALL_ROOT" \ |
| 16 | + && "$INSTALL_ROOT" == "$INSTALL_PREFIX"* \ |
| 17 | + && "$(dirname "$INSTALL_ROOT")" == "$USER_APPLICATIONS_DIR" \ |
| 18 | + && "$INSTALLED_APP" == "$INSTALL_ROOT/CallbackProbe.app" ]]; then |
| 19 | + "$LSREGISTER_PATH" -u "$INSTALLED_APP" >/dev/null 2>&1 || true |
| 20 | + if [[ -d "$INSTALL_ROOT" ]]; then |
| 21 | + rm -rf -- "$INSTALL_ROOT" |
| 22 | + fi |
| 23 | + fi |
| 24 | + if [[ -n "$WORK_DIR" && "$WORK_DIR" == "$WORK_PREFIX"* && -d "$WORK_DIR" ]]; then |
| 25 | + rm -rf -- "$WORK_DIR" |
| 26 | + fi |
| 27 | +} |
| 28 | +trap cleanup EXIT |
| 29 | + |
| 30 | +test -x "$LSREGISTER_PATH" |
| 31 | +test -f "$HANDLER_VERIFIER" |
| 32 | +mkdir -p "$USER_APPLICATIONS_DIR" |
| 33 | + |
| 34 | +WORK_DIR="$(mktemp -d "${WORK_PREFIX}XXXXXX")" |
| 35 | +INSTALL_ROOT="$(mktemp -d "${INSTALL_PREFIX}XXXXXX")" |
| 36 | +SOURCE_APP="$WORK_DIR/CallbackProbe.app" |
| 37 | +INSTALLED_APP="$INSTALL_ROOT/CallbackProbe.app" |
| 38 | +SCHEME="aidictationhandlercontract${RANDOM}${RANDOM}" |
| 39 | + |
| 40 | +mkdir -p "$SOURCE_APP/Contents/MacOS" |
| 41 | +cat > "$WORK_DIR/CallbackProbe.swift" <<'SWIFT' |
| 42 | +import AppKit |
| 43 | +import Foundation |
| 44 | +
|
| 45 | +final class AppDelegate: NSObject, NSApplicationDelegate { |
| 46 | + func application(_ application: NSApplication, open urls: [URL]) { |
| 47 | + let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") |
| 48 | + as? [[String: Any]] |
| 49 | + let expectedScheme = (urlTypes?.first?["CFBundleURLSchemes"] as? [String])?.first |
| 50 | + guard urls.contains(where: { |
| 51 | + $0.scheme == expectedScheme && $0.host == "auth-callback" |
| 52 | + }) else { |
| 53 | + return |
| 54 | + } |
| 55 | +
|
| 56 | + let sentinel = Bundle.main.bundleURL |
| 57 | + .deletingLastPathComponent() |
| 58 | + .appendingPathComponent("callback-received") |
| 59 | + try? Data("received\n".utf8).write(to: sentinel, options: .atomic) |
| 60 | + application.terminate(nil) |
| 61 | + } |
| 62 | +
|
| 63 | + func applicationDidFinishLaunching(_ notification: Notification) { |
| 64 | + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { |
| 65 | + NSApplication.shared.terminate(nil) |
| 66 | + } |
| 67 | + } |
| 68 | +} |
| 69 | +
|
| 70 | +let app = NSApplication.shared |
| 71 | +let delegate = AppDelegate() |
| 72 | +app.delegate = delegate |
| 73 | +app.setActivationPolicy(.prohibited) |
| 74 | +app.run() |
| 75 | +SWIFT |
| 76 | + |
| 77 | +cat > "$SOURCE_APP/Contents/Info.plist" <<PLIST |
| 78 | +<?xml version="1.0" encoding="UTF-8"?> |
| 79 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 80 | +<plist version="1.0"> |
| 81 | +<dict> |
| 82 | + <key>CFBundleExecutable</key> |
| 83 | + <string>CallbackProbe</string> |
| 84 | + <key>CFBundleIdentifier</key> |
| 85 | + <string>com.writingmate.aidictation.release-handler-contract</string> |
| 86 | + <key>CFBundleName</key> |
| 87 | + <string>CallbackProbe</string> |
| 88 | + <key>CFBundlePackageType</key> |
| 89 | + <string>APPL</string> |
| 90 | + <key>CFBundleURLTypes</key> |
| 91 | + <array> |
| 92 | + <dict> |
| 93 | + <key>CFBundleTypeRole</key> |
| 94 | + <string>Editor</string> |
| 95 | + <key>CFBundleURLSchemes</key> |
| 96 | + <array> |
| 97 | + <string>$SCHEME</string> |
| 98 | + </array> |
| 99 | + </dict> |
| 100 | + </array> |
| 101 | +</dict> |
| 102 | +</plist> |
| 103 | +PLIST |
| 104 | + |
| 105 | +swiftc "$WORK_DIR/CallbackProbe.swift" -o "$SOURCE_APP/Contents/MacOS/CallbackProbe" |
| 106 | +codesign --force --deep --sign - "$SOURCE_APP" >/dev/null |
| 107 | + |
| 108 | +if swift "$HANDLER_VERIFIER" "$SOURCE_APP" "$SCHEME" 0.25 >/dev/null 2>&1; then |
| 109 | + echo "Synthetic callback unexpectedly resolved to the uninstalled source app." >&2 |
| 110 | + exit 1 |
| 111 | +fi |
| 112 | + |
| 113 | +ditto "$SOURCE_APP" "$INSTALLED_APP" |
| 114 | +codesign --verify --deep --strict "$INSTALLED_APP" |
| 115 | +"$LSREGISTER_PATH" -f "$INSTALLED_APP" |
| 116 | +swift "$HANDLER_VERIFIER" "$INSTALLED_APP" "$SCHEME" |
| 117 | +if ! open "$SCHEME://auth-callback" >/dev/null 2>&1; then |
| 118 | + echo "macOS could not deliver the synthetic callback to the registered app." >&2 |
| 119 | + exit 1 |
| 120 | +fi |
| 121 | +for attempt in {1..20}; do |
| 122 | + if [[ -s "$INSTALL_ROOT/callback-received" ]]; then |
| 123 | + break |
| 124 | + fi |
| 125 | + if [[ "$attempt" -eq 20 ]]; then |
| 126 | + echo "The registered synthetic app did not receive the callback." >&2 |
| 127 | + exit 1 |
| 128 | + fi |
| 129 | + sleep 0.25 |
| 130 | +done |
| 131 | + |
| 132 | +cleanup |
| 133 | +trap - EXIT |
| 134 | +if [[ -e "$INSTALL_ROOT" || -e "$WORK_DIR" ]]; then |
| 135 | + echo "Synthetic callback verification did not clean up its temporary apps." >&2 |
| 136 | + exit 1 |
| 137 | +fi |
| 138 | + |
| 139 | +echo "Verified exact LaunchServices registration, app callback receipt, and cleanup." |
0 commit comments