import { systemPreferences, shell } from 'electron'; import type { ILogger } from '../../../application/ports/ILogger'; import { NoOpLogAdapter } from '../logging/NoOpLogAdapter'; /** * Permission status for macOS automation permissions. */ export interface PermissionStatus { accessibility: boolean; screenRecording: boolean; platform: NodeJS.Platform; } /** * Result of a permission check operation. */ export interface PermissionCheckResult { granted: boolean; status: PermissionStatus; missingPermissions: string[]; } /** * Service for checking and managing macOS permissions required for automation. * * On macOS, the following permissions are required: * - Accessibility: Required for keyboard/mouse control (nut.js) * - Screen Recording: Required for screen capture and window detection * * On other platforms, permissions are assumed to be granted. */ export class PermissionService { private logger: ILogger; private cachedStatus: PermissionStatus | null = null; constructor(logger?: ILogger) { this.logger = logger ?? new NoOpLogAdapter(); } /** * Check if all required permissions are granted. * * @returns PermissionCheckResult with status of each permission */ async checkPermissions(): Promise { const status = await this.getPermissionStatus(); const missingPermissions: string[] = []; if (!status.accessibility) { missingPermissions.push('Accessibility'); } if (!status.screenRecording) { missingPermissions.push('Screen Recording'); } const granted = missingPermissions.length === 0; this.logger.info('Permission check completed', { granted, accessibility: status.accessibility, screenRecording: status.screenRecording, platform: status.platform, }); return { granted, status, missingPermissions, }; } /** * Get the current permission status for each required permission. */ async getPermissionStatus(): Promise { const platform = process.platform; // On non-macOS platforms, assume permissions are granted if (platform !== 'darwin') { this.logger.debug('Non-macOS platform, assuming permissions granted', { platform }); return { accessibility: true, screenRecording: true, platform, }; } const accessibility = this.checkAccessibilityPermission(); const screenRecording = this.checkScreenRecordingPermission(); this.cachedStatus = { accessibility, screenRecording, platform, }; this.logger.debug('Permission status retrieved', this.cachedStatus); return this.cachedStatus; } /** * Check if Accessibility permission is granted. * Uses systemPreferences.isTrustedAccessibilityClient on macOS. */ private checkAccessibilityPermission(): boolean { try { // isTrustedAccessibilityClient checks if the app has Accessibility permission // Pass false to just check without prompting the user const isTrusted = systemPreferences.isTrustedAccessibilityClient(false); this.logger.debug('Accessibility permission check', { isTrusted }); return isTrusted; } catch (error) { this.logger.warn('Failed to check Accessibility permission', { error: error instanceof Error ? error.message : String(error), }); return false; } } /** * Check if Screen Recording permission is granted. * Uses systemPreferences.getMediaAccessStatus on macOS. */ private checkScreenRecordingPermission(): boolean { try { // getMediaAccessStatus with 'screen' checks Screen Recording permission const status = systemPreferences.getMediaAccessStatus('screen'); const isGranted = status === 'granted'; this.logger.debug('Screen Recording permission check', { status, isGranted }); return isGranted; } catch (error) { this.logger.warn('Failed to check Screen Recording permission', { error: error instanceof Error ? error.message : String(error), }); return false; } } /** * Request Accessibility permission by prompting the user. * This will show a system dialog asking for permission. * * @returns true if permission was granted after prompt */ requestAccessibilityPermission(): boolean { if (process.platform !== 'darwin') { return true; } try { // Pass true to prompt the user if not already trusted const isTrusted = systemPreferences.isTrustedAccessibilityClient(true); this.logger.info('Accessibility permission requested', { isTrusted }); return isTrusted; } catch (error) { this.logger.error('Failed to request Accessibility permission', error instanceof Error ? error : new Error(String(error))); return false; } } /** * Open System Preferences to the Security & Privacy pane. * * @param pane - Which pane to open: 'accessibility' or 'screenRecording' */ async openSystemPreferences(pane: 'accessibility' | 'screenRecording'): Promise { if (process.platform !== 'darwin') { this.logger.debug('Not on macOS, cannot open System Preferences'); return; } // macOS System Preferences URLs const urls: Record = { accessibility: 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility', screenRecording: 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture', }; const url = urls[pane]; if (url) { this.logger.info('Opening System Preferences', { pane, url }); await shell.openExternal(url); } } /** * Open System Preferences to show all missing permissions. * Opens Accessibility pane first if that permission is missing. */ async openPermissionsSettings(): Promise { const status = this.cachedStatus ?? await this.getPermissionStatus(); if (!status.accessibility) { await this.openSystemPreferences('accessibility'); } else if (!status.screenRecording) { await this.openSystemPreferences('screenRecording'); } } /** * Get cached permission status without re-checking. * Returns null if permissions haven't been checked yet. */ getCachedStatus(): PermissionStatus | null { return this.cachedStatus; } /** * Clear the cached permission status. * Next call to getPermissionStatus will re-check permissions. */ clearCache(): void { this.cachedStatus = null; } }