136 lines
3.4 KiB
TypeScript
136 lines
3.4 KiB
TypeScript
|
|
/**
|
||
|
|
* 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();
|