import { join } from 'node:path' import { hostname } from 'node:os' import { BrowserWindow, ipcMain, safeStorage, shell } from 'electron' import { MobileVault, LiveTokenIssuer } from './mobile-secrets.mjs' import { DeliveryJournal, MobileGatewayClient } from './mobile-adapter.mjs' import { AccountClient } from '../../mobile/src/transport.mjs' export function createMobileWindow({ directory, assets, resolveBackend, show = true }) { const vault = new MobileVault(join(directory, 'mobile'), safeStorage) const journal = new DeliveryJournal(join(directory, 'mobile', 'deliveries')) const issuer = new LiveTokenIssuer(vault) const clients = new Map() const window = new BrowserWindow({ width: 560, height: 800, show, title: 'AITURK Mobil bağlantısı', webPreferences: { preload: join(assets, 'mobile-preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false } }) window.setMenu(null) window.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) window.webContents.on('will-navigate', event => event.preventDefault()) const api = async () => new AccountClient(await vault.load()) const call = async (path, body, method) => (await api()).call(path, body, method) const send = frame => { if (!window.isDestroyed()) window.webContents.send('aiturk-mobile-event', frame) } const handler = async (event, action, params = {}) => { if (event.sender !== window.webContents || event.senderFrame !== window.webContents.mainFrame) throw new Error('Geçersiz mobil pencere.') if (action === 'setup') { const secrets = await vault.load() return { paired: Boolean(secrets.deviceId), hasAccount: Boolean(secrets.accountToken), hasGoogle: Boolean(secrets.googleKey) } } if (action === 'account.save') { if (typeof params.token !== 'string' || params.token.length < 12 || params.token.length > 512) throw new Error('AITURK hesap anahtarı geçersiz.') if (clients.size) throw new Error('Hesap değiştirmeden önce mobil bağlantıyı kapatın.') const previous = await vault.load() if (previous.accountToken && previous.accountToken !== params.token) throw new Error('Hesabı değiştirmek için önce mevcut cihazı iptal edin ve bağlantı penceresini yeniden açın.') await vault.save({ ...previous, accountToken: params.token }) return { saved: true } } if (action === 'desktop.pair') { const result = await call('/v1/aiturk/pairings', { display_name: hostname(), platform: 'windows-x64' }) await vault.save({ ...await vault.load(), deviceId: result.device_id, deviceToken: result.device_token, pairingId: result.pairing_id }) await shell.openExternal('https://turkservis.online' + result.verification_uri) return { code: result.user_code, expiresAt: result.expires_at } } if (action === 'phone.approve') { if (!/^[0-9a-f-]{36}$/.test(params.pairingId) || !/^[A-Z2-9]{8}$/.test(params.code)) throw new Error('Telefondaki eşleştirme bilgileri geçersiz.') return call(`/v1/aiturk/pairings/${params.pairingId}/approve`, { user_code: params.code }) } if (action === 'devices') return call('/v1/aiturk/mobile/devices') if (action === 'device.revoke') { if (!/^[0-9a-f-]{36}$/.test(params.deviceId) || !Number.isInteger(params.version)) throw new Error('Geçersiz cihaz.') const result = await call(`/v1/aiturk/devices/${params.deviceId}/revoke`, { expected_version: params.version }) for (const [id, entry] of clients) if (entry.phoneId === params.deviceId || result.self_revoked) { entry.client.close(); clients.delete(id); send({ channelId: id, close: true }) } if (result.self_revoked) await vault.save({}) return result } if (action === 'inbox') return call('/v1/aiturk/mobile/channels') if (action === 'ice') return call('/v1/aiturk/mobile/ice') if (action === 'channel.answer') { const inbox = await call('/v1/aiturk/mobile/channels') const offer = inbox.channels.find(row => row.id === params.channelId) if (!offer || clients.has(offer.id) || clients.size >= 4) throw new Error('Bağlantı isteği artık geçerli değil.') const authorize = () => call(`/v1/aiturk/mobile/channels/${offer.id}/heartbeat`, {}) const client = new MobileGatewayClient(resolveBackend, frame => send({ channelId: offer.id, frame }), { deviceId: offer.phone_id, journal, authorize }) await call(`/v1/aiturk/mobile/channels/${offer.id}/answer`, { sdp: params.sdp }) clients.set(offer.id, { client, phoneId: offer.phone_id, authorize }) return { accepted: true } } if (action === 'channel.heartbeat') { const entry = clients.get(params.channelId) if (!entry) throw new Error('Bağlantı yok.') return entry.authorize() } if (action === 'channel.close') { clients.get(params.channelId)?.client.close(); clients.delete(params.channelId) return { closed: true } } if (action === 'channel.request') { const entry = clients.get(params.channelId) if (!entry) throw new Error('Bağlantı yok.') await entry.authorize() const message = params.message if (!message || JSON.stringify(message).length > 65000) throw new Error('İstek boyutu geçersiz.') if (message.action === 'google.key.save') { const key = message.params?.key if (typeof key !== 'string' || !/^[A-Za-z0-9_-]{20,256}$/.test(key)) throw new Error('Google anahtarı biçimi geçersiz.') await vault.save({ ...await vault.load(), googleKey: key }) return { saved: true } } if (message.action === 'google.key.delete') { const secrets = await vault.load(); delete secrets.googleKey; await vault.save(secrets); return { deleted: true } } if (message.action === 'voice.token') return issuer.issue(entry.phoneId) return entry.client.handle(message) } throw new Error('Desteklenmeyen mobil işlem.') } ipcMain.handle('aiturk-mobile', handler) window.on('closed', () => { for (const entry of clients.values()) entry.client.close(); clients.clear(); ipcMain.removeHandler('aiturk-mobile') }) void window.loadFile(join(assets, 'mobile-host.html')) return window }