import fs from 'node:fs/promises'; import http from 'node:http'; import https from 'node:https'; import { WebSocketServer } from 'ws'; import { getPairingCode, getTrustedDeviceCount, initializeAuth, verifyToken } from './auth.js'; import { refreshCodexCache, refreshProjectCache } from './codex-data.js'; import { sendJson } from './http-utils.js'; import { startVoiceRealtimeProxy } from './realtime-voice.js'; import { handleApi } from './api-router.js'; import { HOST, HTTPS_PFX_PASSPHRASE, HTTPS_PFX_PATH, HTTPS_PORT, PORT, PUBLIC_URL } from './app-config.js'; import { remoteAddress } from './app-auth.js'; import { loadFeishuAuthState } from './app-feishu-oauth.js'; import { loadRecentImagePrompts } from './app-image-prompts.js'; import { addBrowserSocket, broadcast } from './app-sockets.js'; import { publicStatus } from './app-status.js'; import { serveStatic } from './app-static.js'; async function requestHandler(req, res) { const url = new URL(req.url || '/', `http://${req.headers.host || `127.0.0.1:${PORT}`}`); try { if (url.pathname.startsWith('/api/')) { await handleApi(req, res, url); return; } await serveStatic(req, res, url); } catch (error) { console.error('[server] Request failed:', error); sendJson(res, 500, { error: error.message || 'Internal server error' }); } } function createUpgradeHandler(wss, realtimeWss) { return async (req, socket, head) => { const url = new URL(req.url || '/', `http://${req.headers.host || `127.0.0.1:${PORT}`}`); if (url.pathname !== '/ws' && url.pathname !== '/ws/realtime') { socket.destroy(); return; } const token = url.searchParams.get('token') || ''; const ok = await verifyToken(token, { remoteAddress: remoteAddress(req) }); if (!ok) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; } if (url.pathname === '/ws/realtime') { realtimeWss.handleUpgrade(req, socket, head, (ws) => { startVoiceRealtimeProxy(ws, { remoteAddress: remoteAddress(req) }); }); return; } wss.handleUpgrade(req, socket, head, async (ws) => { addBrowserSocket(ws); ws.send(JSON.stringify({ type: 'connected', status: await publicStatus(true) })); }); }; } async function startHttpServer(handleUpgrade) { const auth = await initializeAuth(); await refreshProjectCache().catch((error) => console.warn('[sync] Initial project refresh failed:', error.message)); const server = http.createServer(requestHandler); server.on('upgrade', handleUpgrade); server.listen(PORT, HOST, () => { console.log(`CodexMobile listening on http://${HOST}:${PORT}`); console.log(`Pairing code: ${getPairingCode()} (${getTrustedDeviceCount()} trusted device(s)${auth.fixedPairingCode ? ', fixed' : ''})`); console.log('Use Tailscale and open http://:3321 on iPhone.'); refreshCodexCache() .then((snapshot) => broadcast({ type: 'sync-complete', syncedAt: snapshot.syncedAt, projects: snapshot.projects })) .catch((error) => console.warn('[sync] Initial cache refresh failed:', error.message)); }); } async function startHttpsServer(handleUpgrade) { try { const pfx = await fs.readFile(HTTPS_PFX_PATH); const httpsServer = https.createServer({ pfx, passphrase: HTTPS_PFX_PASSPHRASE }, requestHandler); httpsServer.on('upgrade', handleUpgrade); httpsServer.listen(HTTPS_PORT, HOST, () => { console.log(`CodexMobile HTTPS listening on https://${HOST}:${HTTPS_PORT}`); if (PUBLIC_URL) { console.log(`Public/private URL: ${PUBLIC_URL}`); } else { console.log(`Use Tailscale HTTPS: https://..ts.net:${HTTPS_PORT}/`); } }); } catch (error) { if (error.code === 'ENOENT') { console.log(`CodexMobile HTTPS disabled: certificate not found at ${HTTPS_PFX_PATH}`); } else { console.warn(`[server] Failed to start HTTPS listener: ${error.message}`); } } } async function main() { await loadFeishuAuthState(); await loadRecentImagePrompts(); const handleUpgrade = createUpgradeHandler( new WebSocketServer({ noServer: true }), new WebSocketServer({ noServer: true }) ); await startHttpServer(handleUpgrade); await startHttpsServer(handleUpgrade); } main().catch((error) => { console.error('[server] Failed to start:', error); process.exitCode = 1; });