/** * Standalone test agent — connects to the tunnel server and forwards * inbound requests to a local port. Use this to validate Phase 1 * before the Electron app exists. * * Usage: * TUNNEL_SERVER=ws://localhost:3002 \ * TUNNEL_ID= \ * TOKEN= \ * LOCAL_PORT=11434 \ * npm run agent */ import 'dotenv/config'; import WebSocket from 'ws'; import http from 'http'; const SERVER = process.env.TUNNEL_SERVER ?? 'ws://localhost:3002'; const TUNNEL_ID = process.env.TUNNEL_ID ?? ''; const TOKEN = process.env.TOKEN ?? ''; const LOCAL_PORT = Number( process.env.LOCAL_PORT ?? 11434 ); const RECONNECT_DELAY_MS = 3_000; if ( !TUNNEL_ID || !TOKEN ) { console.error( 'TUNNEL_ID and TOKEN env vars are required' ); process.exit( 1 ); } interface RelayRequest { reqId: string; method: string; path: string; headers: Record; body: string; // base64 } interface RelayResponse { reqId: string; status: number; headers: Record; body: string; // base64 } function forward( req: RelayRequest, ws: WebSocket ): void { const bodyBuf = Buffer.from( req.body, 'base64' ); const options: http.RequestOptions = { hostname: 'localhost', port: LOCAL_PORT, path: req.path, method: req.method, headers: { ...req.headers, 'content-length': String( bodyBuf.length ), }, }; const chunks: Buffer[] = []; const localReq = http.request( options, ( localRes ) => { localRes.on( 'data', chunk => chunks.push( Buffer.from( chunk ) ) ); localRes.on( 'end', () => { const respHeaders: Record = {}; for ( const [ k, v ] of Object.entries( localRes.headers ) ) { if ( typeof v === 'string' ) respHeaders[ k ] = v; else if ( Array.isArray( v ) ) respHeaders[ k ] = v.join( ', ' ); } const resp: RelayResponse = { reqId: req.reqId, status: localRes.statusCode ?? 200, headers: respHeaders, body: Buffer.concat( chunks ).toString( 'base64' ), }; ws.send( JSON.stringify( resp ) ); } ); } ); localReq.on( 'error', ( err ) => { console.error( `[agent] local request failed: ${ err.message }` ); const errResp: RelayResponse = { reqId: req.reqId, status: 502, headers: { 'content-type': 'application/json' }, body: Buffer.from( JSON.stringify( { error: 'Local service error', detail: err.message } ) ) .toString( 'base64' ), }; ws.send( JSON.stringify( errResp ) ); } ); if ( bodyBuf.length > 0 ) localReq.write( bodyBuf ); localReq.end(); } function connect(): void { const url = `${ SERVER }/api/agent/${ TUNNEL_ID }?token=${ TOKEN }`; const ws = new WebSocket( url ); ws.on( 'open', () => console.log( `[agent] connected → forwarding to localhost:${ LOCAL_PORT }` ) ); ws.on( 'message', ( data ) => { try { const req: RelayRequest = JSON.parse( data.toString() ); console.log( `[agent] → ${ req.method } ${ req.path }` ); forward( req, ws ); } catch ( err ) { console.error( '[agent] failed to parse message:', err ); } } ); ws.on( 'close', ( code, reason ) => { console.log( `[agent] disconnected (${ code }: ${ reason }). Reconnecting in ${ RECONNECT_DELAY_MS / 1000 }s...` ); setTimeout( connect, RECONNECT_DELAY_MS ); } ); ws.on( 'error', ( err ) => console.error( '[agent] error:', err.message ) ); } connect();