tunnel/scripts/test-agent.ts

136 lines
3.4 KiB
TypeScript
Raw Permalink Normal View History

2026-07-16 12:17:00 +00:00
/**
* 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=<uuid> \
* TOKEN=<accessToken> \
* 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<string, string>;
body: string; // base64
}
interface RelayResponse
{
reqId: string;
status: number;
headers: Record<string, string>;
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<string, string> = {};
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();