import { completeIce, rtcConfiguration, sendFrame } from './mobile/transport.mjs' import { drawQr, InviteScanner } from './mobile/qr.mjs' import { createDeviceIdentity, loadDeviceIdentity, assertRemoteIdentity, parsePhoneIdentity } from './mobile/peer-identity.mjs' const host = window.mobileHost, $ = id => document.getElementById(id), peers = new Map() const status = text => { $('status').textContent = text } function action(id, work) { $(id).onclick = async () => { $(id).disabled = true; try { await work() } catch (error) { status(error.message) } finally { $(id).disabled = false } } } const identityScanner = new InviteScanner(document.createElement('video'), async raw => { if (raw.length > 700) throw new Error('Telefon kimlik belgesi çok büyük.') const value = JSON.parse(raw) const pairingId = $('pairing').value.trim() || value.pairingId parsePhoneIdentity(raw, pairingId) if (!/^[0-9a-f-]{36}$/.test(pairingId)) throw new Error('Eşleştirme kimliği geçersiz.') $('pairing').value = pairingId; $('phone-identity').value = raw status('Telefon kimliği QR resminden alındı. Telefondaki kısa kodu girin ve bu telefonu ayrıca onaylayın; henüz izin verilmedi.') }) $('phone-identity-file').onchange = async event => { try { await identityScanner.image(event.target.files[0]) } catch (error) { status(error.message) } finally { event.target.value = '' } } action('login-start', async () => { const result = await host.call('account.login.start'); $('login-code').textContent = `Açılan hesap sayfasında yalnız bu kodu onaylayın: ${result.code}` }) action('login-check', async () => { const result = await host.call('account.login.check'); status(result.approved ? 'Hesaba giriş yapıldı. Bu bilgisayarı eşleştirebilirsiniz.' : 'Tarayıcıdaki kod onayı bekleniyor.') }) action('logout', async () => { await host.call('account.logout'); listening = false; $('login-code').textContent = ''; status('Mobil oturum kapatıldı; hesap ve Google anahtarı bu bilgisayar kasasından temizlendi.') }) async function desktopIdentity(create = false) { const setup = await host.call('setup') if (!setup.deviceId) throw new Error('Önce bu bilgisayarı hesapla eşleştirin.') if (setup.localFingerprint) return loadDeviceIdentity(setup.deviceId, setup.localFingerprint) if (!create) throw new Error('Önce bu bilgisayarda yeni telefon QR kodu oluşturun.') const identity = await createDeviceIdentity(setup.deviceId) await host.call('identity.save', { deviceId: setup.deviceId, fingerprint: identity.fingerprint }) return identity } action('invite', async () => { const identity = await desktopIdentity(true), invite = await host.call('invite.create') drawQr($('invite-qr'), JSON.stringify({ ...invite, version: 2, desktopFingerprint: identity.fingerprint })) $('invite-qr').hidden = false $('invite-status').textContent = `Telefonda aynı hesaba giriş yapıp bu QR kodunu tarayın. Son geçerlilik: ${invite.expiresAt}` }) action('invite-check', async () => { const result = await host.call('invite.check'); if (!result.pending) { status('QR ile gelen telefon henüz yok.'); return }; $('pairing').value = result.pending.pairingId; status(`${result.pending.name} bulundu. Telefondaki kısa kodu girip “Bu telefonu onayla” seçin.`) }) action('save', async () => { const token = $('account').value; $('account').value = ''; await host.call('account.save', { token }); status('Hesap anahtarı Windows kasasına kaydedildi.') }) action('pair', async () => { const pair = await host.call('desktop.pair'); $('code').textContent = `Açılan hesap sayfasında bu kodu onaylayın: ${pair.code}. Süre: ${pair.expiresAt}` }) action('approve', async () => { await host.call('phone.approve', { pairingId: $('pairing').value.trim(), code: $('phone-code').value.trim().toUpperCase(), identity: $('phone-identity').value.trim() }); $('phone-code').value = ''; $('phone-identity').value = ''; status('Telefon bu bilgisayarda kimliğiyle onaylandı.') }) action('refresh', async () => { const data = await host.call('devices'); $('devices').replaceChildren() for (const device of data.devices) { const row = document.createElement('p'), label = document.createElement('span'), button = document.createElement('button') label.textContent = `${device.name} · ${device.platform}${device.self ? ' · Bu bilgisayar' : ''}` button.textContent = 'Cihazı iptal et' button.onclick = async () => { button.disabled = true; try { await host.call('device.revoke', { deviceId: device.id, version: device.version }); row.remove(); status('Cihaz iptal edildi.') } catch (error) { status(error.message); button.disabled = false } } row.append(label, button); $('devices').append(row) } }) function close(id) { const entry = peers.get(id); if (!entry) return; peers.delete(id); clearInterval(entry.timer); entry.pc.close(); void host.call('channel.close', { channelId: id }) } host.onEvent(event => { if (event.close) { close(event.channelId); return }; const entry = peers.get(event.channelId); if (entry?.channel?.readyState === 'open') sendFrame(entry.channel, event.frame) }) async function connect(offer) { assertRemoteIdentity(offer.sdp, offer.trustedPhoneFingerprint) const ice = await host.call('ice') const identity = await desktopIdentity() const pc = new RTCPeerConnection({ ...rtcConfiguration(ice.iceServers), certificates: [identity.certificate] }), entry = { pc }; peers.set(offer.id, entry) pc.ondatachannel = event => { const channel = event.channel; entry.channel = channel if (channel.label !== 'aiturk-mobile-v1') { close(offer.id); return } let pending = 0 channel.onmessage = async event => { if (typeof event.data !== 'string' || event.data.length > 65000 || ++pending > 8) { close(offer.id); return } let message try { message = JSON.parse(event.data) const result = await host.call('channel.request', { channelId: offer.id, message }) if (channel.readyState === 'open') sendFrame(channel, { id: message.id, result }) } catch (error) { if (message?.id && channel.readyState === 'open') channel.send(JSON.stringify({ id: message.id, error: error.message })) } finally { pending-- } } channel.onclose = () => close(offer.id) channel.onopen = () => status('Telefon şifreli veri kanalıyla bağlı.') } try { await pc.setRemoteDescription({ type: 'offer', sdp: offer.sdp }) await pc.setLocalDescription(await pc.createAnswer()); await completeIce(pc) await host.call('channel.answer', { channelId: offer.id, sdp: pc.localDescription.sdp }) entry.timer = setInterval(() => { void host.call('channel.heartbeat', { channelId: offer.id }).catch(() => close(offer.id)) }, 10000) } catch (error) { close(offer.id); throw error } } let listening = false action('listen', async () => { if (listening) return listening = true; status('Onaylı telefon bağlantısı bekleniyor.') while (listening) { try { const data = await host.call('inbox'); for (const offer of data.channels) if (!peers.has(offer.id)) await connect(offer) } catch (error) { status(error.message); listening = false; break } await new Promise(resolve => setTimeout(resolve, 2000)) } }) window.addEventListener('pagehide', () => { listening = false; for (const id of peers.keys()) close(id) })