feat(aiturk): phone link for AITURK Mobil (1.0.0-beta.15)

Settings gets a Phone link window: when enabled, the IDE main process runs a
small TLS proxy on the local network (self-signed certificate, pinned by its
fingerprint) in front of the loopback backend and pairs a phone by QR code
or an 8-character code. Each phone gets its own revocable token; the backend
session token never leaves the computer. The root page, terminal sockets and
secret or settings-changing endpoints are not reachable from a phone. The
old WebRTC pairing window is replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-04 06:02:02 +03:00
1 parent 25d8c18c10
commit 03d3803dfc
16 files changed
+3351 -275

No files matched your search

+88 -8
View File
@@ -9,6 +9,7 @@ import tls from 'node:tls'
import { pathToFileURL, fileURLToPath } from 'node:url'
import { startChromeBridge } from './chrome-bridge.mjs'
import { createMobileWindow } from './mobile-host.mjs'
import { MobileVault } from './mobile-secrets.mjs'
import {
app,
@@ -34,6 +35,14 @@ import {
import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state'
import { createAiturkCliRunner, createAiturkLogin } from './aiturk-login'
import {
createMobileLink,
createMobileLinkFileStore,
createWindowsFirewall,
defaultRouteAddress,
type MobileLink,
selectLanAddresses
} from './mobile-lan-proxy'
import { describeVcRuntimeResult, ensureWindowsVcRuntime } from './windows-vc-runtime'
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
import { appIconCandidates, resolveAppIcon } from './app-icon'
@@ -17818,19 +17827,90 @@ function openMobileConnectionWindow() {
return
}
mobileConnectionWindow = createMobileWindow({
directory: path.join(app.getPath('userData'), 'aiturk-mobile'),
assets: path.dirname(fileURLToPath(import.meta.url)),
resolveBackend: async () => {
const profile = primaryProfileKey()
if (globalRemoteActive() || profileHasRemoteOverride(profile)) throw new Error('Mobil bağlantı için yerel IDE bağlantısını seçin.')
return ensureBackend(profile)
}
})
assets: path.dirname(fileURLToPath(import.meta.url)),
link: getMobileLink()
})
}
// "Telefon bağlantısı" (electron/mobile-lan-proxy.ts): TLS LAN proxy in front of
// the loopback backend + pairing. One controller for the whole app run; it only
// listens while the feature is on and a phone is paired or being paired.
let mobileLink: MobileLink | null = null
function mobileLinkRemoteMode() {
return globalRemoteActive() || profileHasRemoteOverride(primaryProfileKey())
}
function getMobileLink(): MobileLink {
if (mobileLink) {
return mobileLink
}
const directory = path.join(app.getPath('userData'), 'aiturk-mobile', 'mobile')
mobileLink = createMobileLink({
// Same vault file as before (mobile-secrets.bin, safeStorage); older fields are kept.
vault: new MobileVault(directory, safeStorage),
store: createMobileLinkFileStore(directory, path.join(directory, 'lan-link.json'), fs.promises),
resolveBackend: async () => {
if (mobileLinkRemoteMode()) {
throw new Error('aiturk-mobile:remote_mode')
}
const connection = await ensureBackend(primaryProfileKey())
if (connection.mode !== 'local') {
throw new Error('aiturk-mobile:remote_mode')
}
return { baseUrl: connection.baseUrl, token: connection.token }
},
isRemoteMode: mobileLinkRemoteMode,
firewall: createWindowsFirewall({ spawn, platform: process.platform, env: process.env }),
fetch: (url, init) => fetch(url, init as RequestInit),
addresses: async () => selectLanAddresses(os.networkInterfaces(), await defaultRouteAddress()),
version: app.getVersion(),
pcName: os.hostname(),
platform: process.platform,
// Method + path + status only (no token, code, query or body). Successful
// API traffic is not logged; pairing and refusals are, for diagnosis.
log: line => {
if (/ \/aiturk\//.test(line) || / [45]\d\d$/.test(line)) {
rememberLog(`[mobile-lan] ${line}`)
}
}
})
return mobileLink
}
// Only the primary window's top-level frame may open the phone window.
ipcMain.handle('aiturk:mobile:open', async event => {
assertAiturkLoginSender(event)
if (process.platform !== 'win32') {
throw new Error('aiturk-mobile:unsupported')
}
openMobileConnectionWindow()
return { ok: true }
})
app.on('will-quit', () => {
void mobileLink?.stop().catch(() => undefined)
})
app.whenReady().then(() => {
// Explicit development/beta entry point; never enables remote access on boot.
if (process.platform === 'win32' && process.argv.includes('--aiturk-mobile')) openMobileConnectionWindow()
// Resume the phone link only when the user turned it on earlier AND a phone
// is paired; otherwise nothing listens (and no vault / UAC prompt is touched).
if (process.platform === 'win32') {
void getMobileLink()
.start()
.catch(error => rememberLog(`[mobile-lan] start failed: ${error instanceof Error ? error.message : 'error'}`))
}
if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
void startChromeBridge({
directory: path.join(process.env.LOCALAPPDATA, 'TurkServis', 'AITURK-IDE'),