WIP encryption cleanup + providerSetting refactor
This commit is contained in:
		
							parent
							
								
									94ae002d83
								
							
						
					
					
						commit
						ab23c10006
					
				@ -11,6 +11,7 @@ const presetRepository = require('../features/common/repositories/preset');
 | 
				
			|||||||
const askService = require('../features/ask/askService');
 | 
					const askService = require('../features/ask/askService');
 | 
				
			||||||
const listenService = require('../features/listen/listenService');
 | 
					const listenService = require('../features/listen/listenService');
 | 
				
			||||||
const permissionService = require('../features/common/services/permissionService');
 | 
					const permissionService = require('../features/common/services/permissionService');
 | 
				
			||||||
 | 
					const encryptionService = require('../features/common/services/encryptionService');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  // Renderer로부터의 요청을 수신하고 서비스로 전달
 | 
					  // Renderer로부터의 요청을 수신하고 서비스로 전달
 | 
				
			||||||
@ -20,7 +21,6 @@ module.exports = {
 | 
				
			|||||||
    ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
 | 
					    ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
 | 
				
			||||||
    ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));  
 | 
					    ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));  
 | 
				
			||||||
    ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
 | 
					    ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
 | 
				
			||||||
    ipcMain.handle('settings:validate-and-save-key', async (e, { provider, key }) => await settingsService.validateAndSaveKey(provider, key));
 | 
					 | 
				
			||||||
    ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider));
 | 
					    ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider));
 | 
				
			||||||
    ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId));    
 | 
					    ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId));    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,8 +37,13 @@ module.exports = {
 | 
				
			|||||||
    ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
 | 
					    ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
 | 
				
			||||||
    ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
 | 
					    ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
 | 
				
			||||||
    ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
 | 
					    ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
 | 
				
			||||||
    ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
 | 
					    ipcMain.handle('mark-keychain-completed', async () => await permissionService.markKeychainCompleted());
 | 
				
			||||||
    ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
 | 
					    ipcMain.handle('check-keychain-completed', async () => await permissionService.checkKeychainCompleted());
 | 
				
			||||||
 | 
					    ipcMain.handle('initialize-encryption-key', async () => {
 | 
				
			||||||
 | 
					        const userId = authService.getCurrentUserId();
 | 
				
			||||||
 | 
					        await encryptionService.initializeKey(userId);
 | 
				
			||||||
 | 
					        return { success: true };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // User/Auth
 | 
					    // User/Auth
 | 
				
			||||||
    ipcMain.handle('get-current-user', () => authService.getCurrentUser());
 | 
					    ipcMain.handle('get-current-user', () => authService.getCurrentUser());
 | 
				
			||||||
@ -109,6 +114,7 @@ module.exports = {
 | 
				
			|||||||
    ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
 | 
					    ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
 | 
				
			||||||
    ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
 | 
					    ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
 | 
				
			||||||
    ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
 | 
					    ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
 | 
				
			||||||
 | 
					    ipcMain.handle('model:re-initialize-state', () => modelStateService.initialize());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log('[FeatureBridge] Initialized with all feature handlers.');
 | 
					    console.log('[FeatureBridge] Initialized with all feature handlers.');
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
@ -117,6 +117,12 @@ const LATEST_SCHEMA = {
 | 
				
			|||||||
            { name: 'accelerator', type: 'TEXT NOT NULL' },
 | 
					            { name: 'accelerator', type: 'TEXT NOT NULL' },
 | 
				
			||||||
            { name: 'created_at', type: 'INTEGER' }
 | 
					            { name: 'created_at', type: 'INTEGER' }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    permissions: {
 | 
				
			||||||
 | 
					        columns: [
 | 
				
			||||||
 | 
					            { name: 'uid', type: 'TEXT PRIMARY KEY' },
 | 
				
			||||||
 | 
					            { name: 'keychain_completed', type: 'INTEGER DEFAULT 0' }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,6 @@ function getRepository() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    markPermissionsAsCompleted: (...args) => getRepository().markPermissionsAsCompleted(...args),
 | 
					    markKeychainCompleted: (...args) => getRepository().markKeychainCompleted(...args),
 | 
				
			||||||
    checkPermissionsCompleted: (...args) => getRepository().checkPermissionsCompleted(...args),
 | 
					    checkKeychainCompleted: (...args) => getRepository().checkKeychainCompleted(...args),
 | 
				
			||||||
}; 
 | 
					}; 
 | 
				
			||||||
@ -1,14 +1,18 @@
 | 
				
			|||||||
const sqliteClient = require('../../services/sqliteClient');
 | 
					const sqliteClient = require('../../services/sqliteClient');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function markPermissionsAsCompleted() {
 | 
					function markKeychainCompleted(uid) {
 | 
				
			||||||
    return sqliteClient.markPermissionsAsCompleted();
 | 
					    return sqliteClient.query(
 | 
				
			||||||
 | 
					        'INSERT OR REPLACE INTO permissions (uid, keychain_completed) VALUES (?, 1)',
 | 
				
			||||||
 | 
					        [uid]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function checkPermissionsCompleted() {
 | 
					function checkKeychainCompleted(uid) {
 | 
				
			||||||
    return sqliteClient.checkPermissionsCompleted();
 | 
					    const row = sqliteClient.query('SELECT keychain_completed FROM permissions WHERE uid = ?', [uid]);
 | 
				
			||||||
 | 
					    return row.length > 0 && row[0].keychain_completed === 1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    markPermissionsAsCompleted,
 | 
					    markKeychainCompleted,
 | 
				
			||||||
    checkPermissionsCompleted,
 | 
					    checkKeychainCompleted
 | 
				
			||||||
}; 
 | 
					}; 
 | 
				
			||||||
@ -56,6 +56,13 @@ const providerSettingsRepositoryAdapter = {
 | 
				
			|||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        const uid = authService.getCurrentUserId();
 | 
				
			||||||
        return await repo.removeAllByUid(uid);
 | 
					        return await repo.removeAllByUid(uid);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getRawApiKeysByUid() {
 | 
				
			||||||
 | 
					        // This function should always target the local sqlite DB,
 | 
				
			||||||
 | 
					        // as it's part of the local-first boot sequence.
 | 
				
			||||||
 | 
					        const uid = authService.getCurrentUserId();
 | 
				
			||||||
 | 
					        return await sqliteRepository.getRawApiKeysByUid(uid);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,6 @@ function getByProvider(uid, provider) {
 | 
				
			|||||||
    const result = stmt.get(uid, provider) || null;
 | 
					    const result = stmt.get(uid, provider) || null;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (result && result.api_key) {
 | 
					    if (result && result.api_key) {
 | 
				
			||||||
        // Decrypt API key if it exists
 | 
					 | 
				
			||||||
        result.api_key = encryptionService.decrypt(result.api_key);
 | 
					        result.api_key = encryptionService.decrypt(result.api_key);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@ -19,7 +18,6 @@ function getAllByUid(uid) {
 | 
				
			|||||||
    const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
 | 
					    const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
 | 
				
			||||||
    const results = stmt.all(uid);
 | 
					    const results = stmt.all(uid);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Decrypt API keys for all results
 | 
					 | 
				
			||||||
    return results.map(result => {
 | 
					    return results.map(result => {
 | 
				
			||||||
        if (result.api_key) {
 | 
					        if (result.api_key) {
 | 
				
			||||||
            result.api_key = encryptionService.decrypt(result.api_key);
 | 
					            result.api_key = encryptionService.decrypt(result.api_key);
 | 
				
			||||||
@ -31,12 +29,6 @@ function getAllByUid(uid) {
 | 
				
			|||||||
function upsert(uid, provider, settings) {
 | 
					function upsert(uid, provider, settings) {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Encrypt API key if it exists
 | 
					 | 
				
			||||||
    const encryptedSettings = { ...settings };
 | 
					 | 
				
			||||||
    if (encryptedSettings.api_key) {
 | 
					 | 
				
			||||||
        encryptedSettings.api_key = encryptionService.encrypt(encryptedSettings.api_key);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
 | 
					    // Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
        INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, created_at, updated_at)
 | 
					        INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, created_at, updated_at)
 | 
				
			||||||
@ -51,11 +43,11 @@ function upsert(uid, provider, settings) {
 | 
				
			|||||||
    const result = stmt.run(
 | 
					    const result = stmt.run(
 | 
				
			||||||
        uid,
 | 
					        uid,
 | 
				
			||||||
        provider,
 | 
					        provider,
 | 
				
			||||||
        encryptedSettings.api_key || null,
 | 
					        settings.api_key || null,
 | 
				
			||||||
        encryptedSettings.selected_llm_model || null,
 | 
					        settings.selected_llm_model || null,
 | 
				
			||||||
        encryptedSettings.selected_stt_model || null,
 | 
					        settings.selected_stt_model || null,
 | 
				
			||||||
        encryptedSettings.created_at || Date.now(),
 | 
					        settings.created_at || Date.now(),
 | 
				
			||||||
        encryptedSettings.updated_at
 | 
					        settings.updated_at
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return { changes: result.changes };
 | 
					    return { changes: result.changes };
 | 
				
			||||||
@ -75,10 +67,17 @@ function removeAllByUid(uid) {
 | 
				
			|||||||
    return { changes: result.changes };
 | 
					    return { changes: result.changes };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getRawApiKeysByUid(uid) {
 | 
				
			||||||
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
 | 
					    const stmt = db.prepare('SELECT api_key FROM provider_settings WHERE uid = ?');
 | 
				
			||||||
 | 
					    return stmt.all(uid);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    getByProvider,
 | 
					    getByProvider,
 | 
				
			||||||
    getAllByUid,
 | 
					    getAllByUid,
 | 
				
			||||||
    upsert,
 | 
					    upsert,
 | 
				
			||||||
    remove,
 | 
					    remove,
 | 
				
			||||||
    removeAllByUid
 | 
					    removeAllByUid,
 | 
				
			||||||
 | 
					    getRawApiKeysByUid
 | 
				
			||||||
}; 
 | 
					}; 
 | 
				
			||||||
@ -7,6 +7,7 @@ const migrationService = require('./migrationService');
 | 
				
			|||||||
const sessionRepository = require('../repositories/session');
 | 
					const sessionRepository = require('../repositories/session');
 | 
				
			||||||
const providerSettingsRepository = require('../repositories/providerSettings');
 | 
					const providerSettingsRepository = require('../repositories/providerSettings');
 | 
				
			||||||
const userModelSelectionsRepository = require('../repositories/userModelSelections');
 | 
					const userModelSelectionsRepository = require('../repositories/userModelSelections');
 | 
				
			||||||
 | 
					const permissionService = require('./permissionService');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getVirtualKeyByEmail(email, idToken) {
 | 
					async function getVirtualKeyByEmail(email, idToken) {
 | 
				
			||||||
    if (!idToken) {
 | 
					    if (!idToken) {
 | 
				
			||||||
@ -43,7 +44,6 @@ class AuthService {
 | 
				
			|||||||
        this.isInitialized = false;
 | 
					        this.isInitialized = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // This ensures the key is ready before any login/logout state change.
 | 
					        // This ensures the key is ready before any login/logout state change.
 | 
				
			||||||
        encryptionService.initializeKey(this.currentUserId);
 | 
					 | 
				
			||||||
        this.initializationPromise = null;
 | 
					        this.initializationPromise = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sessionRepository.setAuthService(this);
 | 
					        sessionRepository.setAuthService(this);
 | 
				
			||||||
@ -69,8 +69,12 @@ class AuthService {
 | 
				
			|||||||
                    // Clean up any zombie sessions from a previous run for this user.
 | 
					                    // Clean up any zombie sessions from a previous run for this user.
 | 
				
			||||||
                    await sessionRepository.endAllActiveSessions();
 | 
					                    await sessionRepository.endAllActiveSessions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // ** Initialize encryption key for the logged-in user **
 | 
					                    // ** Initialize encryption key for the logged-in user if permissions are already granted **
 | 
				
			||||||
                    await encryptionService.initializeKey(user.uid);
 | 
					                    if (process.platform === 'darwin' && !(await permissionService.checkKeychainCompleted(this.currentUserId))) {
 | 
				
			||||||
 | 
					                        console.warn('[AuthService] Keychain permission not yet completed for this user. Deferring key initialization.');
 | 
				
			||||||
 | 
					                    } else if (process.platform === 'darwin') {
 | 
				
			||||||
 | 
					                        await encryptionService.initializeKey(user.uid);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // ** Check for and run data migration for the user **
 | 
					                    // ** Check for and run data migration for the user **
 | 
				
			||||||
                    // No 'await' here, so it runs in the background without blocking startup.
 | 
					                    // No 'await' here, so it runs in the background without blocking startup.
 | 
				
			||||||
@ -109,8 +113,12 @@ class AuthService {
 | 
				
			|||||||
                    // End active sessions for the local/default user as well.
 | 
					                    // End active sessions for the local/default user as well.
 | 
				
			||||||
                    await sessionRepository.endAllActiveSessions();
 | 
					                    await sessionRepository.endAllActiveSessions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // ** Initialize encryption key for the default/local user **
 | 
					                    // ** Initialize encryption key for the default/local user if permissions are already granted **
 | 
				
			||||||
                    await encryptionService.initializeKey(this.currentUserId);
 | 
					                    if (process.platform === 'darwin' && !(await permissionService.checkKeychainCompleted(this.currentUserId))) {
 | 
				
			||||||
 | 
					                        console.warn('[AuthService] Keychain permission not yet completed for default user. Deferring key initialization.');
 | 
				
			||||||
 | 
					                    } else if (process.platform === 'darwin') {
 | 
				
			||||||
 | 
					                        await encryptionService.initializeKey(this.currentUserId);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                this.broadcastUserState();
 | 
					                this.broadcastUserState();
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
@ -175,7 +183,6 @@ class AuthService {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    getCurrentUserId() {
 | 
					    getCurrentUserId() {
 | 
				
			||||||
        return this.currentUserId;
 | 
					        return this.currentUserId;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -10,10 +10,13 @@ class DatabaseInitializer {
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        // 최종적으로 사용될 DB 경로 (쓰기 가능한 위치)
 | 
					        // 최종적으로 사용될 DB 경로 (쓰기 가능한 위치)
 | 
				
			||||||
        const userDataPath = app.getPath('userData');
 | 
					        const userDataPath = app.getPath('userData');
 | 
				
			||||||
 | 
					        // In both development and production mode, the database is stored in the userData directory:
 | 
				
			||||||
 | 
					        //   macOS: ~/Library/Application Support/Glass/pickleglass.db
 | 
				
			||||||
 | 
					        //   Windows: %APPDATA%\Glass\pickleglass.db
 | 
				
			||||||
        this.dbPath = path.join(userDataPath, 'pickleglass.db');
 | 
					        this.dbPath = path.join(userDataPath, 'pickleglass.db');
 | 
				
			||||||
        this.dataDir = userDataPath;
 | 
					        this.dataDir = userDataPath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 원본 DB 경로 (패키지 내 읽기 전용 위치)
 | 
					        // The original DB path (read-only location in the package)
 | 
				
			||||||
        this.sourceDbPath = app.isPackaged
 | 
					        this.sourceDbPath = app.isPackaged
 | 
				
			||||||
            ? path.join(process.resourcesPath, 'data', 'pickleglass.db')
 | 
					            ? path.join(process.resourcesPath, 'data', 'pickleglass.db')
 | 
				
			||||||
            : path.join(app.getAppPath(), 'data', 'pickleglass.db');
 | 
					            : path.join(app.getAppPath(), 'data', 'pickleglass.db');
 | 
				
			||||||
@ -52,7 +55,7 @@ class DatabaseInitializer {
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
            this.ensureDatabaseExists();
 | 
					            this.ensureDatabaseExists();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
 | 
					            sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // This single call will now synchronize the schema and then init default data.
 | 
					            // This single call will now synchronize the schema and then init default data.
 | 
				
			||||||
            await sqliteClient.initTables();
 | 
					            await sqliteClient.initTables();
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,8 @@ try {
 | 
				
			|||||||
    keytar = null;
 | 
					    keytar = null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const permissionService = require('./permissionService');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
 | 
					const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
 | 
				
			||||||
let sessionKey = null; // In-memory fallback key
 | 
					let sessionKey = null; // In-memory fallback key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -31,6 +33,8 @@ async function initializeKey(userId) {
 | 
				
			|||||||
        throw new Error('A user ID must be provided to initialize the encryption key.');
 | 
					        throw new Error('A user ID must be provided to initialize the encryption key.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let keyRetrieved = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (keytar) {
 | 
					    if (keytar) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            let key = await keytar.getPassword(SERVICE_NAME, userId);
 | 
					            let key = await keytar.getPassword(SERVICE_NAME, userId);
 | 
				
			||||||
@ -41,6 +45,7 @@ async function initializeKey(userId) {
 | 
				
			|||||||
                console.log(`[EncryptionService] New key securely stored in keychain for ${userId}.`);
 | 
					                console.log(`[EncryptionService] New key securely stored in keychain for ${userId}.`);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
 | 
					                console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
 | 
				
			||||||
 | 
					                keyRetrieved = true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            sessionKey = key;
 | 
					            sessionKey = key;
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
@ -55,7 +60,17 @@ async function initializeKey(userId) {
 | 
				
			|||||||
            sessionKey = crypto.randomBytes(32).toString('hex');
 | 
					            sessionKey = crypto.randomBytes(32).toString('hex');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
 | 
					    // Mark keychain completed in permissions DB if this is the first successful retrieval or storage
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        await permissionService.markKeychainCompleted(userId);
 | 
				
			||||||
 | 
					        if (keyRetrieved) {
 | 
				
			||||||
 | 
					            console.log(`[EncryptionService] Keychain completion marked in DB for ${userId}.`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } catch (permErr) {
 | 
				
			||||||
 | 
					        console.error('[EncryptionService] Failed to mark keychain completion:', permErr);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!sessionKey) {
 | 
					    if (!sessionKey) {
 | 
				
			||||||
        throw new Error('Failed to initialize encryption key.');
 | 
					        throw new Error('Failed to initialize encryption key.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -129,6 +144,7 @@ function decrypt(encryptedText) {
 | 
				
			|||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        // It's common for this to fail if the data is not encrypted (e.g., legacy data).
 | 
					        // It's common for this to fail if the data is not encrypted (e.g., legacy data).
 | 
				
			||||||
        // In that case, we return the original value.
 | 
					        // In that case, we return the original value.
 | 
				
			||||||
 | 
					        console.error('[EncryptionService] Decryption failed:', error);
 | 
				
			||||||
        return encryptedText;
 | 
					        return encryptedText;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -9,13 +9,39 @@ const userModelSelectionsRepository = require('../repositories/userModelSelectio
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Import authService directly (singleton)
 | 
					// Import authService directly (singleton)
 | 
				
			||||||
const authService = require('./authService');
 | 
					const authService = require('./authService');
 | 
				
			||||||
 | 
					const permissionService = require('./permissionService');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function looksEncrypted(str) {
 | 
				
			||||||
 | 
					    if (!str || typeof str !== 'string') return false;
 | 
				
			||||||
 | 
					    // Base64 chars + optional '=' padding
 | 
				
			||||||
 | 
					    if (!/^[A-Za-z0-9+/]+={0,2}$/.test(str)) return false;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const buf = Buffer.from(str, 'base64');
 | 
				
			||||||
 | 
					        // Our AES-GCM cipher text must be at least 32 bytes (IV 16 + TAG 16)
 | 
				
			||||||
 | 
					        return buf.length >= 32;
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ModelStateService extends EventEmitter {
 | 
					class ModelStateService extends EventEmitter {
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
        this.authService = authService;
 | 
					        this.authService = authService;
 | 
				
			||||||
        this.store = new Store({ name: 'pickle-glass-model-state' });
 | 
					        this.store = new Store({ name: 'pickle-glass-model-state' });
 | 
				
			||||||
        this.state = {};
 | 
					        
 | 
				
			||||||
 | 
					        const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, provider) => {
 | 
				
			||||||
 | 
					            acc[provider] = null;
 | 
				
			||||||
 | 
					            return acc;
 | 
				
			||||||
 | 
					        }, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.state = {
 | 
				
			||||||
 | 
					            apiKeys: initialApiKeys,
 | 
				
			||||||
 | 
					            selectedModels: {
 | 
				
			||||||
 | 
					                llm: null,
 | 
				
			||||||
 | 
					                stt: null
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
        this.hasMigrated = false;
 | 
					        this.hasMigrated = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -193,9 +219,19 @@ class ModelStateService extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    async _loadStateForCurrentUser() {
 | 
					    async _loadStateForCurrentUser() {
 | 
				
			||||||
        const userId = this.authService.getCurrentUserId();
 | 
					        const userId = this.authService.getCurrentUserId();
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Initialize encryption service for current user
 | 
					        // Conditionally initialize encryption if old encrypted keys are detected
 | 
				
			||||||
        await encryptionService.initializeKey(userId);
 | 
					        try {
 | 
				
			||||||
 | 
					            const rows = await providerSettingsRepository.getRawApiKeysByUid();
 | 
				
			||||||
 | 
					            if (rows.some(r => looksEncrypted(r.api_key))) {
 | 
				
			||||||
 | 
					                console.log('[ModelStateService] Encrypted keys detected, initializing encryption...');
 | 
				
			||||||
 | 
					                await encryptionService.initializeKey(userId);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                console.log('[ModelStateService] No encrypted keys detected, skipping encryption initialization.');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					            console.warn('[ModelStateService] Error while checking encrypted keys:', err.message);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Try to load from database first
 | 
					        // Try to load from database first
 | 
				
			||||||
        await this._loadStateFromDatabase();
 | 
					        await this._loadStateFromDatabase();
 | 
				
			||||||
@ -263,17 +299,6 @@ class ModelStateService extends EventEmitter {
 | 
				
			|||||||
            apiKeys: { ...this.state.apiKeys }
 | 
					            apiKeys: { ...this.state.apiKeys }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        for (const [provider, key] of Object.entries(stateToSave.apiKeys)) {
 | 
					 | 
				
			||||||
            if (key) {
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    stateToSave.apiKeys[provider] = encryptionService.encrypt(key);
 | 
					 | 
				
			||||||
                } catch (error) {
 | 
					 | 
				
			||||||
                    console.error(`[ModelStateService] Failed to encrypt API key for ${provider}`);
 | 
					 | 
				
			||||||
                    stateToSave.apiKeys[provider] = null;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        this.store.set(`users.${userId}`, stateToSave);
 | 
					        this.store.set(`users.${userId}`, stateToSave);
 | 
				
			||||||
        console.log(`[ModelStateService] State saved to electron-store for user: ${userId}`);
 | 
					        console.log(`[ModelStateService] State saved to electron-store for user: ${userId}`);
 | 
				
			||||||
        this._logCurrentSelection();
 | 
					        this._logCurrentSelection();
 | 
				
			||||||
 | 
				
			|||||||
@ -2,27 +2,28 @@ const { systemPreferences, shell, desktopCapturer } = require('electron');
 | 
				
			|||||||
const permissionRepository = require('../repositories/permission');
 | 
					const permissionRepository = require('../repositories/permission');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PermissionService {
 | 
					class PermissionService {
 | 
				
			||||||
 | 
					  _getAuthService() {
 | 
				
			||||||
 | 
					    return require('./authService');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async checkSystemPermissions() {
 | 
					  async checkSystemPermissions() {
 | 
				
			||||||
    const permissions = {
 | 
					    const permissions = {
 | 
				
			||||||
      microphone: 'unknown',
 | 
					      microphone: 'unknown',
 | 
				
			||||||
      screen: 'unknown',
 | 
					      screen: 'unknown',
 | 
				
			||||||
 | 
					      keychain: 'unknown',
 | 
				
			||||||
      needsSetup: true
 | 
					      needsSetup: true
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      if (process.platform === 'darwin') {
 | 
					      if (process.platform === 'darwin') {
 | 
				
			||||||
        const micStatus = systemPreferences.getMediaAccessStatus('microphone');
 | 
					        permissions.microphone = systemPreferences.getMediaAccessStatus('microphone');
 | 
				
			||||||
        console.log('[Permissions] Microphone status:', micStatus);
 | 
					        permissions.screen = systemPreferences.getMediaAccessStatus('screen');
 | 
				
			||||||
        permissions.microphone = micStatus;
 | 
					        permissions.keychain = await this.checkKeychainCompleted(this._getAuthService().getCurrentUserId()) ? 'granted' : 'unknown';
 | 
				
			||||||
 | 
					        permissions.needsSetup = permissions.microphone !== 'granted' || permissions.screen !== 'granted' || permissions.keychain !== 'granted';
 | 
				
			||||||
        const screenStatus = systemPreferences.getMediaAccessStatus('screen');
 | 
					 | 
				
			||||||
        console.log('[Permissions] Screen status:', screenStatus);
 | 
					 | 
				
			||||||
        permissions.screen = screenStatus;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        permissions.microphone = 'granted';
 | 
					        permissions.microphone = 'granted';
 | 
				
			||||||
        permissions.screen = 'granted';
 | 
					        permissions.screen = 'granted';
 | 
				
			||||||
 | 
					        permissions.keychain = 'granted';
 | 
				
			||||||
        permissions.needsSetup = false;
 | 
					        permissions.needsSetup = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -33,6 +34,7 @@ class PermissionService {
 | 
				
			|||||||
      return {
 | 
					      return {
 | 
				
			||||||
        microphone: 'unknown',
 | 
					        microphone: 'unknown',
 | 
				
			||||||
        screen: 'unknown',
 | 
					        screen: 'unknown',
 | 
				
			||||||
 | 
					        keychain: 'unknown',
 | 
				
			||||||
        needsSetup: true,
 | 
					        needsSetup: true,
 | 
				
			||||||
        error: error.message
 | 
					        error: error.message
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
@ -92,24 +94,24 @@ class PermissionService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async markPermissionsAsCompleted() {
 | 
					  async markKeychainCompleted() {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await permissionRepository.markPermissionsAsCompleted();
 | 
					      await permissionRepository.markKeychainCompleted(this._getAuthService().getCurrentUserId());
 | 
				
			||||||
      console.log('[Permissions] Marked permissions as completed');
 | 
					      console.log('[Permissions] Marked keychain as completed');
 | 
				
			||||||
      return { success: true };
 | 
					      return { success: true };
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('[Permissions] Error marking permissions as completed:', error);
 | 
					      console.error('[Permissions] Error marking keychain as completed:', error);
 | 
				
			||||||
      return { success: false, error: error.message };
 | 
					      return { success: false, error: error.message };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async checkPermissionsCompleted() {
 | 
					  async checkKeychainCompleted(uid) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const completed = await permissionRepository.checkPermissionsCompleted();
 | 
					      const completed = permissionRepository.checkKeychainCompleted(uid);
 | 
				
			||||||
      console.log('[Permissions] Permissions completed status:', completed);
 | 
					      console.log('[Permissions] Keychain completed status:', completed);
 | 
				
			||||||
      return completed;
 | 
					      return completed;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('[Permissions] Error checking permissions completed status:', error);
 | 
					      console.error('[Permissions] Error checking keychain completed status:', error);
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,7 @@ class SQLiteClient {
 | 
				
			|||||||
        return `"${identifier}"`;
 | 
					        return `"${identifier}"`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    synchronizeSchema() {
 | 
					    async synchronizeSchema() {
 | 
				
			||||||
        console.log('[DB Sync] Starting schema synchronization...');
 | 
					        console.log('[DB Sync] Starting schema synchronization...');
 | 
				
			||||||
        const tablesInDb = this.getTablesFromDb();
 | 
					        const tablesInDb = this.getTablesFromDb();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -132,8 +132,8 @@ class SQLiteClient {
 | 
				
			|||||||
        console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
 | 
					        console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    initTables() {
 | 
					    async initTables() {
 | 
				
			||||||
        this.synchronizeSchema();
 | 
					        await this.synchronizeSchema();
 | 
				
			||||||
        this.initDefaultData();
 | 
					        this.initDefaultData();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -166,21 +166,6 @@ class SQLiteClient {
 | 
				
			|||||||
        console.log('Default data initialized.');
 | 
					        console.log('Default data initialized.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    markPermissionsAsCompleted() {
 | 
					 | 
				
			||||||
        return this.query(
 | 
					 | 
				
			||||||
            'INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)',
 | 
					 | 
				
			||||||
            ['permissions_completed', 'true']
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    checkPermissionsCompleted() {
 | 
					 | 
				
			||||||
        const result = this.query(
 | 
					 | 
				
			||||||
            'SELECT value FROM system_settings WHERE key = ?',
 | 
					 | 
				
			||||||
            ['permissions_completed']
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        return result.length > 0 && result[0].value === 'true';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    close() {
 | 
					    close() {
 | 
				
			||||||
        if (this.db) {
 | 
					        if (this.db) {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
 | 
				
			|||||||
@ -44,10 +44,6 @@ async function getModelSettings() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function validateAndSaveKey(provider, key) {
 | 
					 | 
				
			||||||
    return modelStateService.handleValidateKey(provider, key);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function clearApiKey(provider) {
 | 
					async function clearApiKey(provider) {
 | 
				
			||||||
    const success = await modelStateService.handleRemoveApiKey(provider);
 | 
					    const success = await modelStateService.handleRemoveApiKey(provider);
 | 
				
			||||||
    return { success };
 | 
					    return { success };
 | 
				
			||||||
@ -461,7 +457,6 @@ module.exports = {
 | 
				
			|||||||
    setAutoUpdateSetting,
 | 
					    setAutoUpdateSetting,
 | 
				
			||||||
    // Model settings facade
 | 
					    // Model settings facade
 | 
				
			||||||
    getModelSettings,
 | 
					    getModelSettings,
 | 
				
			||||||
    validateAndSaveKey,
 | 
					 | 
				
			||||||
    clearApiKey,
 | 
					    clearApiKey,
 | 
				
			||||||
    setSelectedModel,
 | 
					    setSelectedModel,
 | 
				
			||||||
    // Ollama facade
 | 
					    // Ollama facade
 | 
				
			||||||
 | 
				
			|||||||
@ -69,6 +69,7 @@ contextBridge.exposeInMainWorld('api', {
 | 
				
			|||||||
  headerController: {
 | 
					  headerController: {
 | 
				
			||||||
    // State Management
 | 
					    // State Management
 | 
				
			||||||
    sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
 | 
					    sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
 | 
				
			||||||
 | 
					    reInitializeModelState: () => ipcRenderer.invoke('model:re-initialize-state'),
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Window Management
 | 
					    // Window Management
 | 
				
			||||||
    resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
 | 
					    resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
 | 
				
			||||||
@ -83,7 +84,9 @@ contextBridge.exposeInMainWorld('api', {
 | 
				
			|||||||
    onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
 | 
					    onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
 | 
				
			||||||
    removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
 | 
					    removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
 | 
				
			||||||
    onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
 | 
					    onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
 | 
				
			||||||
    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback)
 | 
					    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback),
 | 
				
			||||||
 | 
					    onForceShowPermissionHeader: (callback) => ipcRenderer.on('force-show-permission-header', callback),
 | 
				
			||||||
 | 
					    removeOnForceShowPermissionHeader: (callback) => ipcRenderer.removeListener('force-show-permission-header', callback)
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // src/ui/app/MainHeader.js
 | 
					  // src/ui/app/MainHeader.js
 | 
				
			||||||
@ -114,7 +117,9 @@ contextBridge.exposeInMainWorld('api', {
 | 
				
			|||||||
    checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
 | 
					    checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
 | 
				
			||||||
    requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
 | 
					    requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
 | 
				
			||||||
    openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
 | 
					    openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
 | 
				
			||||||
    markPermissionsCompleted: () => ipcRenderer.invoke('mark-permissions-completed')
 | 
					    markKeychainCompleted: () => ipcRenderer.invoke('mark-keychain-completed'),
 | 
				
			||||||
 | 
					    checkKeychainCompleted: (uid) => ipcRenderer.invoke('check-keychain-completed', uid),
 | 
				
			||||||
 | 
					    initializeEncryptionKey: () => ipcRenderer.invoke('initialize-encryption-key') // New for keychain
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // src/ui/app/PickleGlassApp.js
 | 
					  // src/ui/app/PickleGlassApp.js
 | 
				
			||||||
 | 
				
			|||||||
@ -48,7 +48,13 @@ class HeaderTransitionManager {
 | 
				
			|||||||
                console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
 | 
					                console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
 | 
				
			||||||
            } else if (type === 'permission') {
 | 
					            } else if (type === 'permission') {
 | 
				
			||||||
                this.permissionHeader = document.createElement('permission-setup');
 | 
					                this.permissionHeader = document.createElement('permission-setup');
 | 
				
			||||||
                this.permissionHeader.continueCallback = () => this.transitionToMainHeader();
 | 
					                this.permissionHeader.continueCallback = async () => {
 | 
				
			||||||
 | 
					                    if (window.api && window.api.headerController) {
 | 
				
			||||||
 | 
					                        console.log('[HeaderController] Re-initializing model state after permission grant...');
 | 
				
			||||||
 | 
					                        await window.api.headerController.reInitializeModelState();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    this.transitionToMainHeader();
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
                this.headerContainer.appendChild(this.permissionHeader);
 | 
					                this.headerContainer.appendChild(this.permissionHeader);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                this.mainHeader = document.createElement('main-header');
 | 
					                this.mainHeader = document.createElement('main-header');
 | 
				
			||||||
@ -121,19 +127,15 @@ class HeaderTransitionManager {
 | 
				
			|||||||
        const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
 | 
					        const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isConfigured) {
 | 
					        if (isConfigured) {
 | 
				
			||||||
            const { isLoggedIn } = userState;
 | 
					            // If providers are configured, always check permissions regardless of login state.
 | 
				
			||||||
            if (isLoggedIn) {
 | 
					            const permissionResult = await this.checkPermissions();
 | 
				
			||||||
                const permissionResult = await this.checkPermissions();
 | 
					            if (permissionResult.success) {
 | 
				
			||||||
                if (permissionResult.success) {
 | 
					 | 
				
			||||||
                    this.transitionToMainHeader();
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    this.transitionToPermissionHeader();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                this.transitionToMainHeader();
 | 
					                this.transitionToMainHeader();
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.transitionToPermissionHeader();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // 프로바이더가 설정되지 않았으면 WelcomeHeader 먼저 표시
 | 
					            // If no providers are configured, show the welcome header to prompt for setup.
 | 
				
			||||||
            await this._resizeForWelcome();
 | 
					            await this._resizeForWelcome();
 | 
				
			||||||
            this.ensureHeader('welcome');
 | 
					            this.ensureHeader('welcome');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -258,6 +258,7 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
    static properties = {
 | 
					    static properties = {
 | 
				
			||||||
        microphoneGranted: { type: String },
 | 
					        microphoneGranted: { type: String },
 | 
				
			||||||
        screenGranted: { type: String },
 | 
					        screenGranted: { type: String },
 | 
				
			||||||
 | 
					        keychainGranted: { type: String },
 | 
				
			||||||
        isChecking: { type: String },
 | 
					        isChecking: { type: String },
 | 
				
			||||||
        continueCallback: { type: Function }
 | 
					        continueCallback: { type: Function }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@ -266,6 +267,7 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
        super();
 | 
					        super();
 | 
				
			||||||
        this.microphoneGranted = 'unknown';
 | 
					        this.microphoneGranted = 'unknown';
 | 
				
			||||||
        this.screenGranted = 'unknown';
 | 
					        this.screenGranted = 'unknown';
 | 
				
			||||||
 | 
					        this.keychainGranted = 'unknown';
 | 
				
			||||||
        this.isChecking = false;
 | 
					        this.isChecking = false;
 | 
				
			||||||
        this.continueCallback = null;
 | 
					        this.continueCallback = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -298,12 +300,14 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
            
 | 
					            
 | 
				
			||||||
            const prevMic = this.microphoneGranted;
 | 
					            const prevMic = this.microphoneGranted;
 | 
				
			||||||
            const prevScreen = this.screenGranted;
 | 
					            const prevScreen = this.screenGranted;
 | 
				
			||||||
 | 
					            const prevKeychain = this.keychainGranted;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            this.microphoneGranted = permissions.microphone;
 | 
					            this.microphoneGranted = permissions.microphone;
 | 
				
			||||||
            this.screenGranted = permissions.screen;
 | 
					            this.screenGranted = permissions.screen;
 | 
				
			||||||
 | 
					            this.keychainGranted = permissions.keychain;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // if permissions changed == UI update
 | 
					            // if permissions changed == UI update
 | 
				
			||||||
            if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted) {
 | 
					            if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted || prevKeychain !== this.keychainGranted) {
 | 
				
			||||||
                console.log('[PermissionHeader] Permission status changed, updating UI');
 | 
					                console.log('[PermissionHeader] Permission status changed, updating UI');
 | 
				
			||||||
                this.requestUpdate();
 | 
					                this.requestUpdate();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -311,6 +315,7 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
            // if all permissions granted == automatically continue
 | 
					            // if all permissions granted == automatically continue
 | 
				
			||||||
            if (this.microphoneGranted === 'granted' && 
 | 
					            if (this.microphoneGranted === 'granted' && 
 | 
				
			||||||
                this.screenGranted === 'granted' && 
 | 
					                this.screenGranted === 'granted' && 
 | 
				
			||||||
 | 
					                this.keychainGranted === 'granted' && 
 | 
				
			||||||
                this.continueCallback) {
 | 
					                this.continueCallback) {
 | 
				
			||||||
                console.log('[PermissionHeader] All permissions granted, proceeding automatically');
 | 
					                console.log('[PermissionHeader] All permissions granted, proceeding automatically');
 | 
				
			||||||
                setTimeout(() => this.handleContinue(), 500);
 | 
					                setTimeout(() => this.handleContinue(), 500);
 | 
				
			||||||
@ -381,17 +386,36 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handleKeychainClick() {
 | 
				
			||||||
 | 
					        if (!window.api || this.keychainGranted === 'granted') return;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        console.log('[PermissionHeader] Requesting keychain permission...');
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Trigger initializeKey to prompt for keychain access
 | 
				
			||||||
 | 
					            // Assuming encryptionService is accessible or via API
 | 
				
			||||||
 | 
					            await window.api.permissionHeader.initializeEncryptionKey(); // New IPC handler needed
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // After success, update status
 | 
				
			||||||
 | 
					            this.keychainGranted = 'granted';
 | 
				
			||||||
 | 
					            this.requestUpdate();
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error('[PermissionHeader] Error requesting keychain permission:', error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async handleContinue() {
 | 
					    async handleContinue() {
 | 
				
			||||||
        if (this.continueCallback && 
 | 
					        if (this.continueCallback && 
 | 
				
			||||||
            this.microphoneGranted === 'granted' && 
 | 
					            this.microphoneGranted === 'granted' && 
 | 
				
			||||||
            this.screenGranted === 'granted') {
 | 
					            this.screenGranted === 'granted' && 
 | 
				
			||||||
 | 
					            this.keychainGranted === 'granted') {
 | 
				
			||||||
            // Mark permissions as completed
 | 
					            // Mark permissions as completed
 | 
				
			||||||
            if (window.api) {
 | 
					            if (window.api) {
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    await window.api.permissionHeader.markPermissionsCompleted();
 | 
					                    await window.api.permissionHeader.markKeychainCompleted();
 | 
				
			||||||
                    console.log('[PermissionHeader] Marked permissions as completed');
 | 
					                    console.log('[PermissionHeader] Marked keychain as completed');
 | 
				
			||||||
                } catch (error) {
 | 
					                } catch (error) {
 | 
				
			||||||
                    console.error('[PermissionHeader] Error marking permissions as completed:', error);
 | 
					                    console.error('[PermissionHeader] Error marking keychain as completed:', error);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
@ -407,7 +431,7 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render() {
 | 
					    render() {
 | 
				
			||||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted';
 | 
					        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && this.keychainGranted === 'granted';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return html`
 | 
					        return html`
 | 
				
			||||||
            <div class="container">
 | 
					            <div class="container">
 | 
				
			||||||
@ -419,7 +443,7 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
                <h1 class="title">Permission Setup Required</h1>
 | 
					                <h1 class="title">Permission Setup Required</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="form-content">
 | 
					                <div class="form-content">
 | 
				
			||||||
                    <div class="subtitle">Grant access to microphone and screen recording to continue</div>
 | 
					                    <div class="subtitle">Grant access to microphone, screen recording and keychain to continue</div>
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    <div class="permission-status">
 | 
					                    <div class="permission-status">
 | 
				
			||||||
                        <div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
 | 
					                        <div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
 | 
				
			||||||
@ -449,6 +473,20 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
                                <span>Screen Recording</span>
 | 
					                                <span>Screen Recording</span>
 | 
				
			||||||
                            `}
 | 
					                            `}
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <div class="permission-item ${this.keychainGranted === 'granted' ? 'granted' : ''}">
 | 
				
			||||||
 | 
					                            ${this.keychainGranted === 'granted' ? html`
 | 
				
			||||||
 | 
					                                <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
 | 
					                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
				
			||||||
 | 
					                                </svg>
 | 
				
			||||||
 | 
					                                <span>Keychain ✓</span>
 | 
				
			||||||
 | 
					                            ` : html`
 | 
				
			||||||
 | 
					                                <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
 | 
					                                    <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.744 5.668l-1.649 1.652c-.63.63-1.706.19-1.706-.742V12.18a.75.75 0 00-1.5 0v2.696c0 .932-1.075 1.372-1.706.742l-1.649-1.652A6 6 0 112 8zm-4 0a.75.75 0 00.75-.75A3.75 3.75 0 018.25 4a.75.75 0 000 1.5 2.25 2.25 0 012.25 2.25.75.75 0 00.75.75z" clip-rule="evenodd" />
 | 
				
			||||||
 | 
					                                </svg>
 | 
				
			||||||
 | 
					                                <span>Keychain Access</span>
 | 
				
			||||||
 | 
					                            `}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ${this.microphoneGranted !== 'granted' ? html`
 | 
					                    ${this.microphoneGranted !== 'granted' ? html`
 | 
				
			||||||
@ -469,6 +507,16 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
                        </button>
 | 
					                        </button>
 | 
				
			||||||
                    ` : ''}
 | 
					                    ` : ''}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ${this.keychainGranted !== 'granted' ? html`
 | 
				
			||||||
 | 
					                        <button 
 | 
				
			||||||
 | 
					                            class="action-button" 
 | 
				
			||||||
 | 
					                            @click=${this.handleKeychainClick}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            Grant Keychain Access
 | 
				
			||||||
 | 
					                        </button>
 | 
				
			||||||
 | 
					                        <div class="subtitle">System prompt will appear. Select 'Always Allow' for seamless access.</div>
 | 
				
			||||||
 | 
					                    ` : ''}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ${allGranted ? html`
 | 
					                    ${allGranted ? html`
 | 
				
			||||||
                        <button 
 | 
					                        <button 
 | 
				
			||||||
                            class="continue-button" 
 | 
					                            class="continue-button" 
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user