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:
1 parent
25d8c18c10
commit
03d3803dfc
16 files changed
+3351
-275
No files matched your search
@@ -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'),
|
||||
|
||||
Reference in new issue
Block a user