🚀 Socket Launch Week Day 4:Socket MCP Adds Org Alerts, Threat Feed Review, and Package Inspection.Learn more
Sign In

@limrun/api

Package Overview
Dependencies
Maintainers
1
Versions
89
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@limrun/api - npm Package Compare versions

Comparing version
0.7.0
to
0.8.0
+68
instance-client.d.mts
import type { Tunnel } from "./tunnel.mjs";
/**
* A client for interacting with a Limbar instance
*/
export type InstanceClient = {
/**
* Take a screenshot of the current screen
* @returns A promise that resolves to the screenshot data
*/
screenshot: () => Promise<ScreenshotData>;
/**
* Disconnect from the Limbar instance
*/
disconnect: () => void;
/**
* Establish an ADB tunnel to the instance.
* Returns the local TCP port and a cleanup function.
*/
startAdbTunnel: () => Promise<Tunnel>;
/**
* Send an asset URL to the instance. The instance will download the asset
* and process it (currently APK install is supported). Resolves on success,
* rejects with an Error on failure.
*/
sendAsset: (url: string) => Promise<void>;
};
/**
* Controls the verbosity of logging in the client
*/
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
/**
* Configuration options for creating an Instance API client
*/
export type InstanceClientOptions = {
/**
* The URL of the ADB WebSocket endpoint.
*/
adbUrl: string;
/**
* The URL of the main endpoint WebSocket.
*/
endpointUrl: string;
/**
* The token to use for the WebSocket connections.
*/
token: string;
/**
* Path to the ADB executable.
* @default 'adb'
*/
adbPath?: string;
/**
* Controls logging verbosity
* @default 'info'
*/
logLevel?: LogLevel;
};
type ScreenshotData = {
dataUri: string;
};
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export declare function createInstanceClient(options: InstanceClientOptions): Promise<InstanceClient>;
export {};
//# sourceMappingURL=instance-client.d.mts.map
{"version":3,"file":"instance-client.d.mts","sourceRoot":"","sources":["src/instance-client.ts"],"names":[],"mappings":"OAIO,KAAK,EAAE,MAAM,EAAE;AAEtB;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC;;;;OAIG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB,CAAC;AAaF,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA0BF;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CA0OlG"}
import type { Tunnel } from "./tunnel.js";
/**
* A client for interacting with a Limbar instance
*/
export type InstanceClient = {
/**
* Take a screenshot of the current screen
* @returns A promise that resolves to the screenshot data
*/
screenshot: () => Promise<ScreenshotData>;
/**
* Disconnect from the Limbar instance
*/
disconnect: () => void;
/**
* Establish an ADB tunnel to the instance.
* Returns the local TCP port and a cleanup function.
*/
startAdbTunnel: () => Promise<Tunnel>;
/**
* Send an asset URL to the instance. The instance will download the asset
* and process it (currently APK install is supported). Resolves on success,
* rejects with an Error on failure.
*/
sendAsset: (url: string) => Promise<void>;
};
/**
* Controls the verbosity of logging in the client
*/
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
/**
* Configuration options for creating an Instance API client
*/
export type InstanceClientOptions = {
/**
* The URL of the ADB WebSocket endpoint.
*/
adbUrl: string;
/**
* The URL of the main endpoint WebSocket.
*/
endpointUrl: string;
/**
* The token to use for the WebSocket connections.
*/
token: string;
/**
* Path to the ADB executable.
* @default 'adb'
*/
adbPath?: string;
/**
* Controls logging verbosity
* @default 'info'
*/
logLevel?: LogLevel;
};
type ScreenshotData = {
dataUri: string;
};
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export declare function createInstanceClient(options: InstanceClientOptions): Promise<InstanceClient>;
export {};
//# sourceMappingURL=instance-client.d.ts.map
{"version":3,"file":"instance-client.d.ts","sourceRoot":"","sources":["src/instance-client.ts"],"names":[],"mappings":"OAIO,KAAK,EAAE,MAAM,EAAE;AAEtB;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC;;;;OAIG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB,CAAC;AAaF,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA0BF;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CA0OlG"}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInstanceClient = createInstanceClient;
const ws_1 = require("ws");
const node_child_process_1 = require("node:child_process");
const tunnel_1 = require("./tunnel.js");
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
async function createInstanceClient(options) {
const serverAddress = `${options.endpointUrl}?token=${options.token}`;
const logLevel = options.logLevel ?? 'info';
let ws = undefined;
const screenshotRequests = new Map();
const assetRequests = new Map();
// Logger functions
const logger = {
debug: (...args) => {
if (logLevel === 'debug')
console.log(...args);
},
info: (...args) => {
if (logLevel === 'info' || logLevel === 'debug')
console.log(...args);
},
warn: (...args) => {
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug')
console.warn(...args);
},
error: (...args) => {
if (logLevel !== 'none')
console.error(...args);
},
};
return new Promise((resolveConnection, rejectConnection) => {
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
ws = new ws_1.WebSocket(serverAddress);
ws.on('message', (data) => {
let message;
try {
message = JSON.parse(data.toString());
}
catch (e) {
logger.error({ data, error: e }, 'Failed to parse JSON message');
return;
}
switch (message.type) {
case 'screenshot': {
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
logger.warn('Received invalid screenshot message:', message);
break;
}
const screenshotMessage = message;
const request = screenshotRequests.get(screenshotMessage.id);
if (!request) {
logger.warn(`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`);
break;
}
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
request.resolver({ dataUri: screenshotMessage.dataUri });
screenshotRequests.delete(screenshotMessage.id);
break;
}
case 'screenshotError': {
if (!('message' in message) || !('id' in message)) {
logger.warn('Received invalid screenshot error message:', message);
break;
}
const errorMessage = message;
const request = screenshotRequests.get(errorMessage.id);
if (!request) {
logger.warn(`Received screenshot error for unknown or already handled session: ${errorMessage.id}`);
break;
}
logger.error(`Server reported an error capturing screenshot for session ${errorMessage.id}:`, errorMessage.message);
request.rejecter(new Error(errorMessage.message));
screenshotRequests.delete(errorMessage.id);
break;
}
case 'assetResult': {
logger.debug('Received assetResult:', message);
const request = assetRequests.get(message.url);
if (!request) {
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
break;
}
if (message.result === 'success') {
logger.debug('Asset result is success');
request.resolver();
assetRequests.delete(message.url);
break;
}
const errorMessage = typeof message.message === 'string' && message.message ?
message.message
: `Asset processing failed: ${JSON.stringify(message)}`;
logger.debug('Asset result is failure', errorMessage);
request.rejecter(new Error(errorMessage));
assetRequests.delete(message.url);
break;
}
default:
logger.warn(`Received unexpected message type: ${message.type}`);
break;
}
});
ws.on('error', (err) => {
logger.error('WebSocket error:', err.message);
if (ws && (ws.readyState === ws_1.WebSocket.CONNECTING || ws.readyState === ws_1.WebSocket.OPEN)) {
rejectConnection(err);
}
screenshotRequests.forEach((request) => request.rejecter(err));
});
ws.on('close', () => {
logger.debug('Disconnected from server.');
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
});
const screenshot = async () => {
if (!ws || ws.readyState !== ws_1.WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const id = 'ts-client-' + Date.now();
const screenshotRequest = {
type: 'screenshot',
id,
};
return new Promise((resolve, reject) => {
logger.debug('Sending screenshot request:', screenshotRequest);
ws.send(JSON.stringify(screenshotRequest), (err) => {
if (err) {
logger.error('Failed to send screenshot request:', err);
reject(err);
}
});
const timeout = setTimeout(() => {
if (screenshotRequests.has(id)) {
logger.error(`Screenshot request timed out for session ${id}`);
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
screenshotRequests.delete(id);
}
}, 30000);
screenshotRequests.set(id, {
resolver: (value) => {
clearTimeout(timeout);
resolve(value);
screenshotRequests.delete(id);
},
rejecter: (reason) => {
clearTimeout(timeout);
reject(reason);
screenshotRequests.delete(id);
},
});
});
};
const disconnect = () => {
if (ws) {
logger.debug('Closing WebSocket connection.');
ws.close();
}
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
};
/**
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
* client to it.
*/
const startAdbTunnel = async () => {
const { address, close } = await (0, tunnel_1.startTcpTunnel)(options.adbUrl, options.token, '127.0.0.1', 0);
try {
await new Promise((resolve, reject) => {
(0, node_child_process_1.exec)(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
if (err)
return reject(err);
resolve();
});
});
logger.debug(`ADB connected on ${address.address}`);
}
catch (err) {
close();
throw err;
}
return { address, close };
};
const sendAsset = async (url) => {
if (!ws || ws.readyState !== ws_1.WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const assetRequest = {
type: 'asset',
url,
};
ws.send(JSON.stringify(assetRequest), (err) => {
if (err) {
logger.error('Failed to send asset request:', err);
}
});
return new Promise((resolve, reject) => {
assetRequests.set(url, { resolver: resolve, rejecter: reject });
});
};
ws.on('open', () => {
logger.debug(`Connected to ${serverAddress}`);
resolveConnection({
screenshot,
disconnect,
startAdbTunnel,
sendAsset,
});
});
});
}
//# sourceMappingURL=instance-client.js.map
{"version":3,"file":"instance-client.js","sourceRoot":"","sources":["src/instance-client.ts"],"names":[],"mappings":"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"}
import { WebSocket } from 'ws';
import { exec } from 'node:child_process';
import { startTcpTunnel } from "./tunnel.mjs";
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export async function createInstanceClient(options) {
const serverAddress = `${options.endpointUrl}?token=${options.token}`;
const logLevel = options.logLevel ?? 'info';
let ws = undefined;
const screenshotRequests = new Map();
const assetRequests = new Map();
// Logger functions
const logger = {
debug: (...args) => {
if (logLevel === 'debug')
console.log(...args);
},
info: (...args) => {
if (logLevel === 'info' || logLevel === 'debug')
console.log(...args);
},
warn: (...args) => {
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug')
console.warn(...args);
},
error: (...args) => {
if (logLevel !== 'none')
console.error(...args);
},
};
return new Promise((resolveConnection, rejectConnection) => {
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
ws = new WebSocket(serverAddress);
ws.on('message', (data) => {
let message;
try {
message = JSON.parse(data.toString());
}
catch (e) {
logger.error({ data, error: e }, 'Failed to parse JSON message');
return;
}
switch (message.type) {
case 'screenshot': {
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
logger.warn('Received invalid screenshot message:', message);
break;
}
const screenshotMessage = message;
const request = screenshotRequests.get(screenshotMessage.id);
if (!request) {
logger.warn(`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`);
break;
}
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
request.resolver({ dataUri: screenshotMessage.dataUri });
screenshotRequests.delete(screenshotMessage.id);
break;
}
case 'screenshotError': {
if (!('message' in message) || !('id' in message)) {
logger.warn('Received invalid screenshot error message:', message);
break;
}
const errorMessage = message;
const request = screenshotRequests.get(errorMessage.id);
if (!request) {
logger.warn(`Received screenshot error for unknown or already handled session: ${errorMessage.id}`);
break;
}
logger.error(`Server reported an error capturing screenshot for session ${errorMessage.id}:`, errorMessage.message);
request.rejecter(new Error(errorMessage.message));
screenshotRequests.delete(errorMessage.id);
break;
}
case 'assetResult': {
logger.debug('Received assetResult:', message);
const request = assetRequests.get(message.url);
if (!request) {
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
break;
}
if (message.result === 'success') {
logger.debug('Asset result is success');
request.resolver();
assetRequests.delete(message.url);
break;
}
const errorMessage = typeof message.message === 'string' && message.message ?
message.message
: `Asset processing failed: ${JSON.stringify(message)}`;
logger.debug('Asset result is failure', errorMessage);
request.rejecter(new Error(errorMessage));
assetRequests.delete(message.url);
break;
}
default:
logger.warn(`Received unexpected message type: ${message.type}`);
break;
}
});
ws.on('error', (err) => {
logger.error('WebSocket error:', err.message);
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
rejectConnection(err);
}
screenshotRequests.forEach((request) => request.rejecter(err));
});
ws.on('close', () => {
logger.debug('Disconnected from server.');
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
});
const screenshot = async () => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const id = 'ts-client-' + Date.now();
const screenshotRequest = {
type: 'screenshot',
id,
};
return new Promise((resolve, reject) => {
logger.debug('Sending screenshot request:', screenshotRequest);
ws.send(JSON.stringify(screenshotRequest), (err) => {
if (err) {
logger.error('Failed to send screenshot request:', err);
reject(err);
}
});
const timeout = setTimeout(() => {
if (screenshotRequests.has(id)) {
logger.error(`Screenshot request timed out for session ${id}`);
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
screenshotRequests.delete(id);
}
}, 30000);
screenshotRequests.set(id, {
resolver: (value) => {
clearTimeout(timeout);
resolve(value);
screenshotRequests.delete(id);
},
rejecter: (reason) => {
clearTimeout(timeout);
reject(reason);
screenshotRequests.delete(id);
},
});
});
};
const disconnect = () => {
if (ws) {
logger.debug('Closing WebSocket connection.');
ws.close();
}
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
};
/**
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
* client to it.
*/
const startAdbTunnel = async () => {
const { address, close } = await startTcpTunnel(options.adbUrl, options.token, '127.0.0.1', 0);
try {
await new Promise((resolve, reject) => {
exec(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
if (err)
return reject(err);
resolve();
});
});
logger.debug(`ADB connected on ${address.address}`);
}
catch (err) {
close();
throw err;
}
return { address, close };
};
const sendAsset = async (url) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const assetRequest = {
type: 'asset',
url,
};
ws.send(JSON.stringify(assetRequest), (err) => {
if (err) {
logger.error('Failed to send asset request:', err);
}
});
return new Promise((resolve, reject) => {
assetRequests.set(url, { resolver: resolve, rejecter: reject });
});
};
ws.on('open', () => {
logger.debug(`Connected to ${serverAddress}`);
resolveConnection({
screenshot,
disconnect,
startAdbTunnel,
sendAsset,
});
});
});
}
//# sourceMappingURL=instance-client.mjs.map
{"version":3,"file":"instance-client.mjs","sourceRoot":"","sources":["src/instance-client.ts"],"names":[],"mappings":"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"}
import { WebSocket, Data } from 'ws';
import { exec } from 'node:child_process';
import { startTcpTunnel } from './tunnel';
import type { Tunnel } from './tunnel';
/**
* A client for interacting with a Limbar instance
*/
export type InstanceClient = {
/**
* Take a screenshot of the current screen
* @returns A promise that resolves to the screenshot data
*/
screenshot: () => Promise<ScreenshotData>;
/**
* Disconnect from the Limbar instance
*/
disconnect: () => void;
/**
* Establish an ADB tunnel to the instance.
* Returns the local TCP port and a cleanup function.
*/
startAdbTunnel: () => Promise<Tunnel>;
/**
* Send an asset URL to the instance. The instance will download the asset
* and process it (currently APK install is supported). Resolves on success,
* rejects with an Error on failure.
*/
sendAsset: (url: string) => Promise<void>;
};
/**
* Controls the verbosity of logging in the client
*/
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
/**
* Configuration options for creating an Instance API client
*/
export type InstanceClientOptions = {
/**
* The URL of the ADB WebSocket endpoint.
*/
adbUrl: string;
/**
* The URL of the main endpoint WebSocket.
*/
endpointUrl: string;
/**
* The token to use for the WebSocket connections.
*/
token: string;
/**
* Path to the ADB executable.
* @default 'adb'
*/
adbPath?: string;
/**
* Controls logging verbosity
* @default 'info'
*/
logLevel?: LogLevel;
};
type ScreenshotRequest = {
type: 'screenshot';
id: string;
};
type ScreenshotResponse = {
type: 'screenshot';
dataUri: string;
id: string;
};
type ScreenshotData = {
dataUri: string;
};
type ScreenshotErrorResponse = {
type: 'screenshotError';
message: string;
id: string;
};
type AssetRequest = {
type: 'asset';
url: string;
};
type AssetResultResponse = {
type: 'assetResult';
result: 'success' | 'failure' | string;
url: string;
message?: string;
};
type ServerMessage =
| ScreenshotResponse
| ScreenshotErrorResponse
| AssetResultResponse
| { type: string; [key: string]: unknown };
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export async function createInstanceClient(options: InstanceClientOptions): Promise<InstanceClient> {
const serverAddress = `${options.endpointUrl}?token=${options.token}`;
const logLevel = options.logLevel ?? 'info';
let ws: WebSocket | undefined = undefined;
const screenshotRequests: Map<
string,
{
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => void;
rejecter: (reason?: any) => void;
}
> = new Map();
const assetRequests: Map<
string,
{
resolver: (value: void | PromiseLike<void>) => void;
rejecter: (reason?: any) => void;
}
> = new Map();
// Logger functions
const logger = {
debug: (...args: any[]) => {
if (logLevel === 'debug') console.log(...args);
},
info: (...args: any[]) => {
if (logLevel === 'info' || logLevel === 'debug') console.log(...args);
},
warn: (...args: any[]) => {
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug') console.warn(...args);
},
error: (...args: any[]) => {
if (logLevel !== 'none') console.error(...args);
},
};
return new Promise<InstanceClient>((resolveConnection, rejectConnection) => {
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
ws = new WebSocket(serverAddress);
ws.on('message', (data: Data) => {
let message: ServerMessage;
try {
message = JSON.parse(data.toString());
} catch (e) {
logger.error({ data, error: e }, 'Failed to parse JSON message');
return;
}
switch (message.type) {
case 'screenshot': {
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
logger.warn('Received invalid screenshot message:', message);
break;
}
const screenshotMessage = message as ScreenshotResponse;
const request = screenshotRequests.get(screenshotMessage.id);
if (!request) {
logger.warn(
`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`,
);
break;
}
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
request.resolver({ dataUri: screenshotMessage.dataUri });
screenshotRequests.delete(screenshotMessage.id);
break;
}
case 'screenshotError': {
if (!('message' in message) || !('id' in message)) {
logger.warn('Received invalid screenshot error message:', message);
break;
}
const errorMessage = message as ScreenshotErrorResponse;
const request = screenshotRequests.get(errorMessage.id);
if (!request) {
logger.warn(
`Received screenshot error for unknown or already handled session: ${errorMessage.id}`,
);
break;
}
logger.error(
`Server reported an error capturing screenshot for session ${errorMessage.id}:`,
errorMessage.message,
);
request.rejecter(new Error(errorMessage.message));
screenshotRequests.delete(errorMessage.id);
break;
}
case 'assetResult': {
logger.debug('Received assetResult:', message);
const request = assetRequests.get(message.url as string);
if (!request) {
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
break;
}
if (message.result === 'success') {
logger.debug('Asset result is success');
request.resolver();
assetRequests.delete(message.url as string);
break;
}
const errorMessage =
typeof message.message === 'string' && message.message ?
message.message
: `Asset processing failed: ${JSON.stringify(message)}`;
logger.debug('Asset result is failure', errorMessage);
request.rejecter(new Error(errorMessage));
assetRequests.delete(message.url as string);
break;
}
default:
logger.warn(`Received unexpected message type: ${message.type}`);
break;
}
});
ws.on('error', (err: Error) => {
logger.error('WebSocket error:', err.message);
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
rejectConnection(err);
}
screenshotRequests.forEach((request) => request.rejecter(err));
});
ws.on('close', () => {
logger.debug('Disconnected from server.');
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
});
const screenshot = async (): Promise<ScreenshotData> => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const id = 'ts-client-' + Date.now();
const screenshotRequest: ScreenshotRequest = {
type: 'screenshot',
id,
};
return new Promise<ScreenshotData>((resolve, reject) => {
logger.debug('Sending screenshot request:', screenshotRequest);
ws!.send(JSON.stringify(screenshotRequest), (err?: Error) => {
if (err) {
logger.error('Failed to send screenshot request:', err);
reject(err);
}
});
const timeout = setTimeout(() => {
if (screenshotRequests.has(id)) {
logger.error(`Screenshot request timed out for session ${id}`);
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
screenshotRequests.delete(id);
}
}, 30000);
screenshotRequests.set(id, {
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => {
clearTimeout(timeout);
resolve(value);
screenshotRequests.delete(id);
},
rejecter: (reason?: any) => {
clearTimeout(timeout);
reject(reason);
screenshotRequests.delete(id);
},
});
});
};
const disconnect = (): void => {
if (ws) {
logger.debug('Closing WebSocket connection.');
ws.close();
}
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
};
/**
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
* client to it.
*/
const startAdbTunnel = async (): Promise<Tunnel> => {
const { address, close } = await startTcpTunnel(options.adbUrl, options.token, '127.0.0.1', 0);
try {
await new Promise<void>((resolve, reject) => {
exec(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
if (err) return reject(err);
resolve();
});
});
logger.debug(`ADB connected on ${address.address}`);
} catch (err) {
close();
throw err;
}
return { address, close };
};
const sendAsset = async (url: string): Promise<void> => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const assetRequest: AssetRequest = {
type: 'asset',
url,
};
ws.send(JSON.stringify(assetRequest), (err?: Error) => {
if (err) {
logger.error('Failed to send asset request:', err);
}
});
return new Promise<void>((resolve, reject) => {
assetRequests.set(url, { resolver: resolve, rejecter: reject });
});
};
ws.on('open', () => {
logger.debug(`Connected to ${serverAddress}`);
resolveConnection({
screenshot,
disconnect,
startAdbTunnel,
sendAsset,
});
});
});
}
import * as net from 'net';
import { WebSocket } from 'ws';
export interface Tunnel {
address: {
address: string;
port: number;
};
close: () => void;
}
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export async function startTcpTunnel(
remoteURL: string,
token: string,
hostname: string,
port: number,
): Promise<Tunnel> {
return new Promise((resolve, reject) => {
const server = net.createServer();
let ws: WebSocket | undefined;
let pingInterval: NodeJS.Timeout | undefined;
// close helper
const close = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = undefined;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'close');
}
if (server.listening) {
server.close();
}
};
// No AbortController support – proxy can be closed via the returned handle
// TCP server error
server.once('error', (err) => {
close();
reject(new Error(`TCP server error: ${err.message}`));
});
// Listening
server.once('listening', () => {
const address = server.address();
if (!address || typeof address === 'string') {
close();
return reject(new Error('Failed to obtain listening address'));
}
resolve({ address, close });
});
// On first TCP connection
server.on('connection', (tcpSocket) => {
// Single-connection proxy
server.close();
ws = new WebSocket(remoteURL, {
headers: { Authorization: `Bearer ${token}` },
perMessageDeflate: false,
});
// WebSocket error
ws.once('error', (err: any) => {
console.error('WebSocket error:', err);
tcpSocket.destroy();
close();
});
ws.once('open', () => {
const socket = ws as WebSocket; // non-undefined after open
pingInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
(socket as any).ping();
}
}, 30_000);
// TCP → WS
tcpSocket.on('data', (chunk) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(chunk);
}
});
// WS → TCP
socket.on('message', (data: any) => {
if (!tcpSocket.destroyed) {
tcpSocket.write(data as Buffer);
}
});
});
// Mutual close
tcpSocket.on('close', close);
tcpSocket.on('error', (err: any) => {
console.error('TCP socket error:', err);
close();
});
ws.on('close', () => tcpSocket.destroy());
});
// Start listening
server.listen(port, hostname);
});
}
export interface Tunnel {
address: {
address: string;
port: number;
};
close: () => void;
}
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export declare function startTcpTunnel(remoteURL: string, token: string, hostname: string, port: number): Promise<Tunnel>;
//# sourceMappingURL=tunnel.d.mts.map
{"version":3,"file":"tunnel.d.mts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA6FjB"}
export interface Tunnel {
address: {
address: string;
port: number;
};
close: () => void;
}
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export declare function startTcpTunnel(remoteURL: string, token: string, hostname: string, port: number): Promise<Tunnel>;
//# sourceMappingURL=tunnel.d.ts.map
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA6FjB"}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startTcpTunnel = startTcpTunnel;
const tslib_1 = require("./internal/tslib.js");
const net = tslib_1.__importStar(require("net"));
const ws_1 = require("ws");
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
async function startTcpTunnel(remoteURL, token, hostname, port) {
return new Promise((resolve, reject) => {
const server = net.createServer();
let ws;
let pingInterval;
// close helper
const close = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = undefined;
}
if (ws && ws.readyState === ws_1.WebSocket.OPEN) {
ws.close(1000, 'close');
}
if (server.listening) {
server.close();
}
};
// No AbortController support – proxy can be closed via the returned handle
// TCP server error
server.once('error', (err) => {
close();
reject(new Error(`TCP server error: ${err.message}`));
});
// Listening
server.once('listening', () => {
const address = server.address();
if (!address || typeof address === 'string') {
close();
return reject(new Error('Failed to obtain listening address'));
}
resolve({ address, close });
});
// On first TCP connection
server.on('connection', (tcpSocket) => {
// Single-connection proxy
server.close();
ws = new ws_1.WebSocket(remoteURL, {
headers: { Authorization: `Bearer ${token}` },
perMessageDeflate: false,
});
// WebSocket error
ws.once('error', (err) => {
console.error('WebSocket error:', err);
tcpSocket.destroy();
close();
});
ws.once('open', () => {
const socket = ws; // non-undefined after open
pingInterval = setInterval(() => {
if (socket.readyState === ws_1.WebSocket.OPEN) {
socket.ping();
}
}, 30000);
// TCP → WS
tcpSocket.on('data', (chunk) => {
if (socket.readyState === ws_1.WebSocket.OPEN) {
socket.send(chunk);
}
});
// WS → TCP
socket.on('message', (data) => {
if (!tcpSocket.destroyed) {
tcpSocket.write(data);
}
});
});
// Mutual close
tcpSocket.on('close', close);
tcpSocket.on('error', (err) => {
console.error('TCP socket error:', err);
close();
});
ws.on('close', () => tcpSocket.destroy());
});
// Start listening
server.listen(port, hostname);
});
}
//# sourceMappingURL=tunnel.js.map
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"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"}
import * as net from 'net';
import { WebSocket } from 'ws';
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export async function startTcpTunnel(remoteURL, token, hostname, port) {
return new Promise((resolve, reject) => {
const server = net.createServer();
let ws;
let pingInterval;
// close helper
const close = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = undefined;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'close');
}
if (server.listening) {
server.close();
}
};
// No AbortController support – proxy can be closed via the returned handle
// TCP server error
server.once('error', (err) => {
close();
reject(new Error(`TCP server error: ${err.message}`));
});
// Listening
server.once('listening', () => {
const address = server.address();
if (!address || typeof address === 'string') {
close();
return reject(new Error('Failed to obtain listening address'));
}
resolve({ address, close });
});
// On first TCP connection
server.on('connection', (tcpSocket) => {
// Single-connection proxy
server.close();
ws = new WebSocket(remoteURL, {
headers: { Authorization: `Bearer ${token}` },
perMessageDeflate: false,
});
// WebSocket error
ws.once('error', (err) => {
console.error('WebSocket error:', err);
tcpSocket.destroy();
close();
});
ws.once('open', () => {
const socket = ws; // non-undefined after open
pingInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.ping();
}
}, 30000);
// TCP → WS
tcpSocket.on('data', (chunk) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(chunk);
}
});
// WS → TCP
socket.on('message', (data) => {
if (!tcpSocket.destroyed) {
tcpSocket.write(data);
}
});
});
// Mutual close
tcpSocket.on('close', close);
tcpSocket.on('error', (err) => {
console.error('TCP socket error:', err);
close();
});
ws.on('close', () => tcpSocket.destroy());
});
// Start listening
server.listen(port, hostname);
});
}
//# sourceMappingURL=tunnel.mjs.map
{"version":3,"file":"tunnel.mjs","sourceRoot":"","sources":["src/tunnel.ts"],"names":[],"mappings":"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"}
+15
-0
# Changelog
## 0.8.0 (2025-09-09)
Full Changelog: [v0.7.0...v0.8.0](https://github.com/limrun-inc/typescript-sdk/compare/v0.7.0...v0.8.0)
### Bug Fixes
* coerce nullable values to undefined ([0b8c51a](https://github.com/limrun-inc/typescript-sdk/commit/0b8c51a581fe97fcc9c2cd7016a3cceee0700d67))
### Chores
* **api:** fix linter issues ([9b65c4f](https://github.com/limrun-inc/typescript-sdk/commit/9b65c4f773a08bd85f23fdcbf2c6e2ae727d7a08))
* **examples:** update to the latest @limrun/api ([a7e513e](https://github.com/limrun-inc/typescript-sdk/commit/a7e513e29c8948109dc0e2cb00d15a030e2cb7a3))
* **tests:** fix the import to be compatible ([40f7fab](https://github.com/limrun-inc/typescript-sdk/commit/40f7fabcf473c28b1a711a8ea524624a3a982e21))
## 0.7.0 (2025-09-08)

@@ -4,0 +19,0 @@

+1
-0

@@ -5,3 +5,4 @@ export { Limrun as default } from "./client.mjs";

export { Limrun, type ClientOptions } from "./client.mjs";
export { createInstanceClient } from "./instance-client.mjs";
export { LimrunError, APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, NotFoundError, ConflictError, RateLimitError, BadRequestError, AuthenticationError, InternalServerError, PermissionDeniedError, UnprocessableEntityError, } from "./core/error.mjs";
//# sourceMappingURL=index.d.mts.map
+1
-1

@@ -1,1 +0,1 @@

{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"OAEO,EAAE,MAAM,IAAI,OAAO,EAAE;OAErB,EAAE,KAAK,UAAU,EAAE,MAAM,EAAE;OAC3B,EAAE,UAAU,EAAE;OACd,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE;OAC9B,EACL,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,GACzB"}
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"OAEO,EAAE,MAAM,IAAI,OAAO,EAAE;OAErB,EAAE,KAAK,UAAU,EAAE,MAAM,EAAE;OAC3B,EAAE,UAAU,EAAE;OACd,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE;OAC9B,EAAE,oBAAoB,EAAE;OACxB,EACL,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,GACzB"}

@@ -5,3 +5,4 @@ export { Limrun as default } from "./client.js";

export { Limrun, type ClientOptions } from "./client.js";
export { createInstanceClient } from "./instance-client.js";
export { LimrunError, APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, NotFoundError, ConflictError, RateLimitError, BadRequestError, AuthenticationError, InternalServerError, PermissionDeniedError, UnprocessableEntityError, } from "./core/error.js";
//# sourceMappingURL=index.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"OAEO,EAAE,MAAM,IAAI,OAAO,EAAE;OAErB,EAAE,KAAK,UAAU,EAAE,MAAM,EAAE;OAC3B,EAAE,UAAU,EAAE;OACd,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE;OAC9B,EACL,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,GACzB"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"OAEO,EAAE,MAAM,IAAI,OAAO,EAAE;OAErB,EAAE,KAAK,UAAU,EAAE,MAAM,EAAE;OAC3B,EAAE,UAAU,EAAE;OACd,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE;OAC9B,EAAE,oBAAoB,EAAE;OACxB,EACL,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,GACzB"}

@@ -7,3 +7,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.UnprocessableEntityError = exports.PermissionDeniedError = exports.InternalServerError = exports.AuthenticationError = exports.BadRequestError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.APIUserAbortError = exports.APIConnectionTimeoutError = exports.APIConnectionError = exports.APIError = exports.LimrunError = exports.Limrun = exports.APIPromise = exports.toFile = exports.default = void 0;
exports.UnprocessableEntityError = exports.PermissionDeniedError = exports.InternalServerError = exports.AuthenticationError = exports.BadRequestError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.APIUserAbortError = exports.APIConnectionTimeoutError = exports.APIConnectionError = exports.APIError = exports.LimrunError = exports.createInstanceClient = exports.Limrun = exports.APIPromise = exports.toFile = exports.default = void 0;
var client_1 = require("./client.js");

@@ -17,2 +17,4 @@ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return client_1.Limrun; } });

Object.defineProperty(exports, "Limrun", { enumerable: true, get: function () { return client_2.Limrun; } });
var instance_client_1 = require("./instance-client.js");
Object.defineProperty(exports, "createInstanceClient", { enumerable: true, get: function () { return instance_client_1.createInstanceClient; } });
var error_1 = require("./core/error.js");

@@ -19,0 +21,0 @@ Object.defineProperty(exports, "LimrunError", { enumerable: true, get: function () { return error_1.LimrunError; } });

@@ -1,1 +0,1 @@

{"version":3,"file":"index.js","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":";AAAA,sFAAsF;;;AAEtF,sCAA6C;AAApC,iGAAA,MAAM,OAAW;AAE1B,6CAAyD;AAA/B,iGAAA,MAAM,OAAA;AAChC,qDAAgD;AAAvC,yGAAA,UAAU,OAAA;AACnB,sCAAsD;AAA7C,gGAAA,MAAM,OAAA;AACf,yCAcsB;AAbpB,oGAAA,WAAW,OAAA;AACX,iGAAA,QAAQ,OAAA;AACR,2GAAA,kBAAkB,OAAA;AAClB,kHAAA,yBAAyB,OAAA;AACzB,0GAAA,iBAAiB,OAAA;AACjB,sGAAA,aAAa,OAAA;AACb,sGAAA,aAAa,OAAA;AACb,uGAAA,cAAc,OAAA;AACd,wGAAA,eAAe,OAAA;AACf,4GAAA,mBAAmB,OAAA;AACnB,4GAAA,mBAAmB,OAAA;AACnB,8GAAA,qBAAqB,OAAA;AACrB,iHAAA,wBAAwB,OAAA"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":";AAAA,sFAAsF;;;AAEtF,sCAA6C;AAApC,iGAAA,MAAM,OAAW;AAE1B,6CAAyD;AAA/B,iGAAA,MAAM,OAAA;AAChC,qDAAgD;AAAvC,yGAAA,UAAU,OAAA;AACnB,sCAAsD;AAA7C,gGAAA,MAAM,OAAA;AACf,wDAAyD;AAAhD,uHAAA,oBAAoB,OAAA;AAC7B,yCAcsB;AAbpB,oGAAA,WAAW,OAAA;AACX,iGAAA,QAAQ,OAAA;AACR,2GAAA,kBAAkB,OAAA;AAClB,kHAAA,yBAAyB,OAAA;AACzB,0GAAA,iBAAiB,OAAA;AACjB,sGAAA,aAAa,OAAA;AACb,sGAAA,aAAa,OAAA;AACb,uGAAA,cAAc,OAAA;AACd,wGAAA,eAAe,OAAA;AACf,4GAAA,mBAAmB,OAAA;AACnB,4GAAA,mBAAmB,OAAA;AACnB,8GAAA,qBAAqB,OAAA;AACrB,iHAAA,wBAAwB,OAAA"}

@@ -6,3 +6,4 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

export { Limrun } from "./client.mjs";
export { createInstanceClient } from "./instance-client.mjs";
export { LimrunError, APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, NotFoundError, ConflictError, RateLimitError, BadRequestError, AuthenticationError, InternalServerError, PermissionDeniedError, UnprocessableEntityError, } from "./core/error.mjs";
//# sourceMappingURL=index.mjs.map

@@ -1,1 +0,1 @@

{"version":3,"file":"index.mjs","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;OAE/E,EAAE,MAAM,IAAI,OAAO,EAAE;OAErB,EAAmB,MAAM,EAAE;OAC3B,EAAE,UAAU,EAAE;OACd,EAAE,MAAM,EAAsB;OAC9B,EACL,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,GACzB"}
{"version":3,"file":"index.mjs","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;OAE/E,EAAE,MAAM,IAAI,OAAO,EAAE;OAErB,EAAmB,MAAM,EAAE;OAC3B,EAAE,UAAU,EAAE;OACd,EAAE,MAAM,EAAsB;OAC9B,EAAE,oBAAoB,EAAE;OACxB,EACL,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,GACzB"}

@@ -83,3 +83,3 @@ "use strict";

const maybeCoerceInteger = (value) => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -91,3 +91,3 @@ }

const maybeCoerceFloat = (value) => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -99,3 +99,3 @@ }

const maybeCoerceBoolean = (value) => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -102,0 +102,0 @@ }

@@ -1,1 +0,1 @@

{"version":3,"file":"values.js","sourceRoot":"","sources":["../../src/internal/utils/values.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"values.js","sourceRoot":"","sources":["../../src/internal/utils/values.ts"],"names":[],"mappings":"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"}

@@ -69,3 +69,3 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

export const maybeCoerceInteger = (value) => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -76,3 +76,3 @@ }

export const maybeCoerceFloat = (value) => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -83,3 +83,3 @@ }

export const maybeCoerceBoolean = (value) => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -86,0 +86,0 @@ }

@@ -1,1 +0,1 @@

{"version":3,"file":"values.mjs","sourceRoot":"","sources":["../../src/internal/utils/values.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"values.mjs","sourceRoot":"","sources":["../../src/internal/utils/values.ts"],"names":[],"mappings":"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"}
{
"name": "@limrun/api",
"version": "0.7.0",
"version": "0.8.0",
"description": "The official TypeScript library for the Limrun API",

@@ -89,2 +89,12 @@ "author": "Limrun <contact@limrun.com>",

},
"./instance-client": {
"import": "./instance-client.mjs",
"require": "./instance-client.js"
},
"./instance-client.js": {
"default": "./instance-client.js"
},
"./instance-client.mjs": {
"default": "./instance-client.mjs"
},
"./resource": {

@@ -120,2 +130,12 @@ "import": "./resource.mjs",

},
"./tunnel": {
"import": "./tunnel.mjs",
"require": "./tunnel.js"
},
"./tunnel.js": {
"default": "./tunnel.js"
},
"./tunnel.mjs": {
"default": "./tunnel.mjs"
},
"./uploads": {

@@ -122,0 +142,0 @@ "import": "./uploads.mjs",

@@ -8,2 +8,3 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

export { Limrun, type ClientOptions } from './client';
export { createInstanceClient } from './instance-client';
export {

@@ -10,0 +11,0 @@ LimrunError,

@@ -79,3 +79,3 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

export const maybeCoerceInteger = (value: unknown): number | undefined => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -87,3 +87,3 @@ }

export const maybeCoerceFloat = (value: unknown): number | undefined => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -95,3 +95,3 @@ }

export const maybeCoerceBoolean = (value: unknown): boolean | undefined => {
if (value === undefined) {
if (value == null) {
return undefined;

@@ -98,0 +98,0 @@ }

@@ -1,1 +0,1 @@

export const VERSION = '0.7.0'; // x-release-please-version
export const VERSION = '0.8.0'; // x-release-please-version

@@ -1,2 +0,2 @@

export declare const VERSION = "0.7.0";
export declare const VERSION = "0.8.0";
//# sourceMappingURL=version.d.mts.map

@@ -1,2 +0,2 @@

export declare const VERSION = "0.7.0";
export declare const VERSION = "0.8.0";
//# sourceMappingURL=version.d.ts.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VERSION = void 0;
exports.VERSION = '0.7.0'; // x-release-please-version
exports.VERSION = '0.8.0'; // x-release-please-version
//# sourceMappingURL=version.js.map

@@ -1,2 +0,2 @@

export const VERSION = '0.7.0'; // x-release-please-version
export const VERSION = '0.8.0'; // x-release-please-version
//# sourceMappingURL=version.mjs.map
import type { Tunnel } from "./tunnel.mjs";
export type { Tunnel } from "./tunnel.mjs";
import { AndroidInstance } from "./android-instances.mjs";
/**
* A client for interacting with a Limbar instance
*/
export type InstanceClient = {
/**
* Take a screenshot of the current screen
* @returns A promise that resolves to the screenshot data
*/
screenshot: () => Promise<ScreenshotData>;
/**
* Disconnect from the Limbar instance
*/
disconnect: () => void;
/**
* Establish an ADB tunnel to the instance.
* Returns the local TCP port and a cleanup function.
*/
startAdbTunnel: () => Promise<Tunnel>;
/**
* Send an asset URL to the instance. The instance will download the asset
* and process it (currently APK install is supported). Resolves on success,
* rejects with an Error on failure.
*/
sendAsset: (url: string) => Promise<void>;
};
/**
* Controls the verbosity of logging in the client
*/
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
/**
* Configuration options for creating an Instance API client
*/
export type InstanceClientOptions = {
/**
* Path to the ADB executable.
* @default 'adb'
*/
adbPath?: string;
/**
* Controls logging verbosity
* @default 'info'
*/
logLevel?: LogLevel;
};
type ScreenshotData = {
dataUri: string;
};
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export declare function createInstanceClient(androidInstance: AndroidInstance, options?: InstanceClientOptions): Promise<InstanceClient>;
//# sourceMappingURL=instance-client.d.mts.map
{"version":3,"file":"instance-client.d.mts","sourceRoot":"","sources":["../src/resources/instance-client.ts"],"names":[],"mappings":"OAIO,KAAK,EAAE,MAAM,EAAE;YACV,EAAE,MAAM,EAAE;OACf,EAAE,eAAe,EAAE;AAE1B;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC;;;;OAIG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB,CAAC;AAaF,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA0BF;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,eAAe,EAAE,eAAe,EAChC,OAAO,GAAE,qBAGR,GACA,OAAO,CAAC,cAAc,CAAC,CAmPzB"}
import type { Tunnel } from "./tunnel.js";
export type { Tunnel } from "./tunnel.js";
import { AndroidInstance } from "./android-instances.js";
/**
* A client for interacting with a Limbar instance
*/
export type InstanceClient = {
/**
* Take a screenshot of the current screen
* @returns A promise that resolves to the screenshot data
*/
screenshot: () => Promise<ScreenshotData>;
/**
* Disconnect from the Limbar instance
*/
disconnect: () => void;
/**
* Establish an ADB tunnel to the instance.
* Returns the local TCP port and a cleanup function.
*/
startAdbTunnel: () => Promise<Tunnel>;
/**
* Send an asset URL to the instance. The instance will download the asset
* and process it (currently APK install is supported). Resolves on success,
* rejects with an Error on failure.
*/
sendAsset: (url: string) => Promise<void>;
};
/**
* Controls the verbosity of logging in the client
*/
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
/**
* Configuration options for creating an Instance API client
*/
export type InstanceClientOptions = {
/**
* Path to the ADB executable.
* @default 'adb'
*/
adbPath?: string;
/**
* Controls logging verbosity
* @default 'info'
*/
logLevel?: LogLevel;
};
type ScreenshotData = {
dataUri: string;
};
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export declare function createInstanceClient(androidInstance: AndroidInstance, options?: InstanceClientOptions): Promise<InstanceClient>;
//# sourceMappingURL=instance-client.d.ts.map
{"version":3,"file":"instance-client.d.ts","sourceRoot":"","sources":["../src/resources/instance-client.ts"],"names":[],"mappings":"OAIO,KAAK,EAAE,MAAM,EAAE;YACV,EAAE,MAAM,EAAE;OACf,EAAE,eAAe,EAAE;AAE1B;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC;;;;OAIG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB,CAAC;AAaF,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA0BF;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,eAAe,EAAE,eAAe,EAChC,OAAO,GAAE,qBAGR,GACA,OAAO,CAAC,cAAc,CAAC,CAmPzB"}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInstanceClient = createInstanceClient;
const ws_1 = require("ws");
const node_child_process_1 = require("node:child_process");
const tunnel_js_1 = require("./tunnel.js");
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
async function createInstanceClient(androidInstance, options = {
adbPath: 'adb',
logLevel: 'info',
}) {
const token = androidInstance.status.token;
const serverAddress = `${androidInstance.status.endpointWebSocketUrl}?token=${token}`;
const logLevel = options.logLevel ?? 'info';
let ws = undefined;
const screenshotRequests = new Map();
const assetRequests = new Map();
// Logger functions
const logger = {
debug: (...args) => {
if (logLevel === 'debug')
console.log(...args);
},
info: (...args) => {
if (logLevel === 'info' || logLevel === 'debug')
console.log(...args);
},
warn: (...args) => {
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug')
console.warn(...args);
},
error: (...args) => {
if (logLevel !== 'none')
console.error(...args);
},
};
return new Promise((resolveConnection, rejectConnection) => {
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
ws = new ws_1.WebSocket(serverAddress);
ws.on('message', (data) => {
let message;
try {
message = JSON.parse(data.toString());
}
catch (e) {
logger.error({ data, error: e }, 'Failed to parse JSON message');
return;
}
switch (message.type) {
case 'screenshot': {
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
logger.warn('Received invalid screenshot message:', message);
break;
}
const screenshotMessage = message;
const request = screenshotRequests.get(screenshotMessage.id);
if (!request) {
logger.warn(`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`);
break;
}
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
request.resolver({ dataUri: screenshotMessage.dataUri });
screenshotRequests.delete(screenshotMessage.id);
break;
}
case 'screenshotError': {
if (!('message' in message) || !('id' in message)) {
logger.warn('Received invalid screenshot error message:', message);
break;
}
const errorMessage = message;
const request = screenshotRequests.get(errorMessage.id);
if (!request) {
logger.warn(`Received screenshot error for unknown or already handled session: ${errorMessage.id}`);
break;
}
logger.error(`Server reported an error capturing screenshot for session ${errorMessage.id}:`, errorMessage.message);
request.rejecter(new Error(errorMessage.message));
screenshotRequests.delete(errorMessage.id);
break;
}
case 'assetResult': {
logger.debug('Received assetResult:', message);
const request = assetRequests.get(message.url);
if (!request) {
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
break;
}
if (message.result === 'success') {
logger.debug('Asset result is success');
request.resolver();
assetRequests.delete(message.url);
break;
}
const errorMessage = typeof message.message === 'string' && message.message ?
message.message
: `Asset processing failed: ${JSON.stringify(message)}`;
logger.debug('Asset result is failure', errorMessage);
request.rejecter(new Error(errorMessage));
assetRequests.delete(message.url);
break;
}
default:
logger.warn(`Received unexpected message type: ${message.type}`);
break;
}
});
ws.on('error', (err) => {
logger.error('WebSocket error:', err.message);
if (ws && (ws.readyState === ws_1.WebSocket.CONNECTING || ws.readyState === ws_1.WebSocket.OPEN)) {
rejectConnection(err);
}
screenshotRequests.forEach((request) => request.rejecter(err));
});
ws.on('close', () => {
logger.debug('Disconnected from server.');
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
});
const screenshot = async () => {
if (!ws || ws.readyState !== ws_1.WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const id = 'ts-client-' + Date.now();
const screenshotRequest = {
type: 'screenshot',
id,
};
return new Promise((resolve, reject) => {
logger.debug('Sending screenshot request:', screenshotRequest);
ws.send(JSON.stringify(screenshotRequest), (err) => {
if (err) {
logger.error('Failed to send screenshot request:', err);
reject(err);
}
});
const timeout = setTimeout(() => {
if (screenshotRequests.has(id)) {
logger.error(`Screenshot request timed out for session ${id}`);
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
screenshotRequests.delete(id);
}
}, 30000);
screenshotRequests.set(id, {
resolver: (value) => {
clearTimeout(timeout);
resolve(value);
screenshotRequests.delete(id);
},
rejecter: (reason) => {
clearTimeout(timeout);
reject(reason);
screenshotRequests.delete(id);
},
});
});
};
const disconnect = () => {
if (ws) {
logger.debug('Closing WebSocket connection.');
ws.close();
}
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
};
/**
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
* client to it.
*/
const startAdbTunnel = async () => {
if (!androidInstance.status.adbWebSocketUrl) {
return Promise.reject(new Error('ADB WebSocket URL is not set'));
}
const { address, close } = await (0, tunnel_js_1.startTcpTunnel)(androidInstance.status.adbWebSocketUrl, token, '127.0.0.1', 0);
try {
await new Promise((resolve, reject) => {
(0, node_child_process_1.exec)(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
if (err)
return reject(err);
resolve();
});
});
logger.debug(`ADB connected on ${address.address}`);
}
catch (err) {
close();
throw err;
}
return { address, close };
};
const sendAsset = async (url) => {
if (!ws || ws.readyState !== ws_1.WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const assetRequest = {
type: 'asset',
url,
};
ws.send(JSON.stringify(assetRequest), (err) => {
if (err) {
logger.error('Failed to send asset request:', err);
}
});
return new Promise((resolve, reject) => {
assetRequests.set(url, { resolver: resolve, rejecter: reject });
});
};
ws.on('open', () => {
logger.debug(`Connected to ${serverAddress}`);
resolveConnection({
screenshot,
disconnect,
startAdbTunnel,
sendAsset,
});
});
});
}
//# sourceMappingURL=instance-client.js.map
{"version":3,"file":"instance-client.js","sourceRoot":"","sources":["../src/resources/instance-client.ts"],"names":[],"mappings":"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"}
import { WebSocket } from 'ws';
import { exec } from 'node:child_process';
import { startTcpTunnel } from "./tunnel.mjs";
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export async function createInstanceClient(androidInstance, options = {
adbPath: 'adb',
logLevel: 'info',
}) {
const token = androidInstance.status.token;
const serverAddress = `${androidInstance.status.endpointWebSocketUrl}?token=${token}`;
const logLevel = options.logLevel ?? 'info';
let ws = undefined;
const screenshotRequests = new Map();
const assetRequests = new Map();
// Logger functions
const logger = {
debug: (...args) => {
if (logLevel === 'debug')
console.log(...args);
},
info: (...args) => {
if (logLevel === 'info' || logLevel === 'debug')
console.log(...args);
},
warn: (...args) => {
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug')
console.warn(...args);
},
error: (...args) => {
if (logLevel !== 'none')
console.error(...args);
},
};
return new Promise((resolveConnection, rejectConnection) => {
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
ws = new WebSocket(serverAddress);
ws.on('message', (data) => {
let message;
try {
message = JSON.parse(data.toString());
}
catch (e) {
logger.error({ data, error: e }, 'Failed to parse JSON message');
return;
}
switch (message.type) {
case 'screenshot': {
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
logger.warn('Received invalid screenshot message:', message);
break;
}
const screenshotMessage = message;
const request = screenshotRequests.get(screenshotMessage.id);
if (!request) {
logger.warn(`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`);
break;
}
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
request.resolver({ dataUri: screenshotMessage.dataUri });
screenshotRequests.delete(screenshotMessage.id);
break;
}
case 'screenshotError': {
if (!('message' in message) || !('id' in message)) {
logger.warn('Received invalid screenshot error message:', message);
break;
}
const errorMessage = message;
const request = screenshotRequests.get(errorMessage.id);
if (!request) {
logger.warn(`Received screenshot error for unknown or already handled session: ${errorMessage.id}`);
break;
}
logger.error(`Server reported an error capturing screenshot for session ${errorMessage.id}:`, errorMessage.message);
request.rejecter(new Error(errorMessage.message));
screenshotRequests.delete(errorMessage.id);
break;
}
case 'assetResult': {
logger.debug('Received assetResult:', message);
const request = assetRequests.get(message.url);
if (!request) {
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
break;
}
if (message.result === 'success') {
logger.debug('Asset result is success');
request.resolver();
assetRequests.delete(message.url);
break;
}
const errorMessage = typeof message.message === 'string' && message.message ?
message.message
: `Asset processing failed: ${JSON.stringify(message)}`;
logger.debug('Asset result is failure', errorMessage);
request.rejecter(new Error(errorMessage));
assetRequests.delete(message.url);
break;
}
default:
logger.warn(`Received unexpected message type: ${message.type}`);
break;
}
});
ws.on('error', (err) => {
logger.error('WebSocket error:', err.message);
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
rejectConnection(err);
}
screenshotRequests.forEach((request) => request.rejecter(err));
});
ws.on('close', () => {
logger.debug('Disconnected from server.');
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
});
const screenshot = async () => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const id = 'ts-client-' + Date.now();
const screenshotRequest = {
type: 'screenshot',
id,
};
return new Promise((resolve, reject) => {
logger.debug('Sending screenshot request:', screenshotRequest);
ws.send(JSON.stringify(screenshotRequest), (err) => {
if (err) {
logger.error('Failed to send screenshot request:', err);
reject(err);
}
});
const timeout = setTimeout(() => {
if (screenshotRequests.has(id)) {
logger.error(`Screenshot request timed out for session ${id}`);
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
screenshotRequests.delete(id);
}
}, 30000);
screenshotRequests.set(id, {
resolver: (value) => {
clearTimeout(timeout);
resolve(value);
screenshotRequests.delete(id);
},
rejecter: (reason) => {
clearTimeout(timeout);
reject(reason);
screenshotRequests.delete(id);
},
});
});
};
const disconnect = () => {
if (ws) {
logger.debug('Closing WebSocket connection.');
ws.close();
}
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
};
/**
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
* client to it.
*/
const startAdbTunnel = async () => {
if (!androidInstance.status.adbWebSocketUrl) {
return Promise.reject(new Error('ADB WebSocket URL is not set'));
}
const { address, close } = await startTcpTunnel(androidInstance.status.adbWebSocketUrl, token, '127.0.0.1', 0);
try {
await new Promise((resolve, reject) => {
exec(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
if (err)
return reject(err);
resolve();
});
});
logger.debug(`ADB connected on ${address.address}`);
}
catch (err) {
close();
throw err;
}
return { address, close };
};
const sendAsset = async (url) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const assetRequest = {
type: 'asset',
url,
};
ws.send(JSON.stringify(assetRequest), (err) => {
if (err) {
logger.error('Failed to send asset request:', err);
}
});
return new Promise((resolve, reject) => {
assetRequests.set(url, { resolver: resolve, rejecter: reject });
});
};
ws.on('open', () => {
logger.debug(`Connected to ${serverAddress}`);
resolveConnection({
screenshot,
disconnect,
startAdbTunnel,
sendAsset,
});
});
});
}
//# sourceMappingURL=instance-client.mjs.map
{"version":3,"file":"instance-client.mjs","sourceRoot":"","sources":["../src/resources/instance-client.ts"],"names":[],"mappings":"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"}
export interface Tunnel {
address: {
address: string;
port: number;
};
close: () => void;
}
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export declare function startTcpTunnel(remoteURL: string, token: string, hostname: string, port: number): Promise<Tunnel>;
//# sourceMappingURL=tunnel.d.mts.map
{"version":3,"file":"tunnel.d.mts","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA6FjB"}
export interface Tunnel {
address: {
address: string;
port: number;
};
close: () => void;
}
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export declare function startTcpTunnel(remoteURL: string, token: string, hostname: string, port: number): Promise<Tunnel>;
//# sourceMappingURL=tunnel.d.ts.map
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA6FjB"}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startTcpTunnel = startTcpTunnel;
const tslib_1 = require("../internal/tslib.js");
const net = tslib_1.__importStar(require("net"));
const ws_1 = require("ws");
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
async function startTcpTunnel(remoteURL, token, hostname, port) {
return new Promise((resolve, reject) => {
const server = net.createServer();
let ws;
let pingInterval;
// close helper
const close = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = undefined;
}
if (ws && ws.readyState === ws_1.WebSocket.OPEN) {
ws.close(1000, 'close');
}
if (server.listening) {
server.close();
}
};
// No AbortController support – proxy can be closed via the returned handle
// TCP server error
server.once('error', (err) => {
close();
reject(new Error(`TCP server error: ${err.message}`));
});
// Listening
server.once('listening', () => {
const address = server.address();
if (!address || typeof address === 'string') {
close();
return reject(new Error('Failed to obtain listening address'));
}
resolve({ address, close });
});
// On first TCP connection
server.on('connection', (tcpSocket) => {
// Single-connection proxy
server.close();
ws = new ws_1.WebSocket(remoteURL, {
headers: { Authorization: `Bearer ${token}` },
perMessageDeflate: false,
});
// WebSocket error
ws.once('error', (err) => {
console.error('WebSocket error:', err);
tcpSocket.destroy();
close();
});
ws.once('open', () => {
const socket = ws; // non-undefined after open
pingInterval = setInterval(() => {
if (socket.readyState === ws_1.WebSocket.OPEN) {
socket.ping();
}
}, 30000);
// TCP → WS
tcpSocket.on('data', (chunk) => {
if (socket.readyState === ws_1.WebSocket.OPEN) {
socket.send(chunk);
}
});
// WS → TCP
socket.on('message', (data) => {
if (!tcpSocket.destroyed) {
tcpSocket.write(data);
}
});
});
// Mutual close
tcpSocket.on('close', close);
tcpSocket.on('error', (err) => {
console.error('TCP socket error:', err);
close();
});
ws.on('close', () => tcpSocket.destroy());
});
// Start listening
server.listen(port, hostname);
});
}
//# sourceMappingURL=tunnel.js.map
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"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"}
import * as net from 'net';
import { WebSocket } from 'ws';
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export async function startTcpTunnel(remoteURL, token, hostname, port) {
return new Promise((resolve, reject) => {
const server = net.createServer();
let ws;
let pingInterval;
// close helper
const close = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = undefined;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'close');
}
if (server.listening) {
server.close();
}
};
// No AbortController support – proxy can be closed via the returned handle
// TCP server error
server.once('error', (err) => {
close();
reject(new Error(`TCP server error: ${err.message}`));
});
// Listening
server.once('listening', () => {
const address = server.address();
if (!address || typeof address === 'string') {
close();
return reject(new Error('Failed to obtain listening address'));
}
resolve({ address, close });
});
// On first TCP connection
server.on('connection', (tcpSocket) => {
// Single-connection proxy
server.close();
ws = new WebSocket(remoteURL, {
headers: { Authorization: `Bearer ${token}` },
perMessageDeflate: false,
});
// WebSocket error
ws.once('error', (err) => {
console.error('WebSocket error:', err);
tcpSocket.destroy();
close();
});
ws.once('open', () => {
const socket = ws; // non-undefined after open
pingInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.ping();
}
}, 30000);
// TCP → WS
tcpSocket.on('data', (chunk) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(chunk);
}
});
// WS → TCP
socket.on('message', (data) => {
if (!tcpSocket.destroyed) {
tcpSocket.write(data);
}
});
});
// Mutual close
tcpSocket.on('close', close);
tcpSocket.on('error', (err) => {
console.error('TCP socket error:', err);
close();
});
ws.on('close', () => tcpSocket.destroy());
});
// Start listening
server.listen(port, hostname);
});
}
//# sourceMappingURL=tunnel.mjs.map
{"version":3,"file":"tunnel.mjs","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"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"}
import { WebSocket, Data } from 'ws';
import { exec } from 'node:child_process';
import { startTcpTunnel } from './tunnel.js';
import type { Tunnel } from './tunnel.js';
export type { Tunnel } from './tunnel.js';
import { AndroidInstance } from './android-instances.js';
/**
* A client for interacting with a Limbar instance
*/
export type InstanceClient = {
/**
* Take a screenshot of the current screen
* @returns A promise that resolves to the screenshot data
*/
screenshot: () => Promise<ScreenshotData>;
/**
* Disconnect from the Limbar instance
*/
disconnect: () => void;
/**
* Establish an ADB tunnel to the instance.
* Returns the local TCP port and a cleanup function.
*/
startAdbTunnel: () => Promise<Tunnel>;
/**
* Send an asset URL to the instance. The instance will download the asset
* and process it (currently APK install is supported). Resolves on success,
* rejects with an Error on failure.
*/
sendAsset: (url: string) => Promise<void>;
};
/**
* Controls the verbosity of logging in the client
*/
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
/**
* Configuration options for creating an Instance API client
*/
export type InstanceClientOptions = {
/**
* Path to the ADB executable.
* @default 'adb'
*/
adbPath?: string;
/**
* Controls logging verbosity
* @default 'info'
*/
logLevel?: LogLevel;
};
type ScreenshotRequest = {
type: 'screenshot';
id: string;
};
type ScreenshotResponse = {
type: 'screenshot';
dataUri: string;
id: string;
};
type ScreenshotData = {
dataUri: string;
};
type ScreenshotErrorResponse = {
type: 'screenshotError';
message: string;
id: string;
};
type AssetRequest = {
type: 'asset';
url: string;
};
type AssetResultResponse = {
type: 'assetResult';
result: 'success' | 'failure' | string;
url: string;
message?: string;
};
type ServerMessage =
| ScreenshotResponse
| ScreenshotErrorResponse
| AssetResultResponse
| { type: string; [key: string]: unknown };
/**
* Creates a client for interacting with a Limbar instance
* @param options Configuration options including webrtcUrl, token and log level
* @returns An InstanceClient for controlling the instance
*/
export async function createInstanceClient(
androidInstance: AndroidInstance,
options: InstanceClientOptions = {
adbPath: 'adb',
logLevel: 'info',
},
): Promise<InstanceClient> {
const token = androidInstance.status.token;
const serverAddress = `${androidInstance.status.endpointWebSocketUrl}?token=${token}`;
const logLevel = options.logLevel ?? 'info';
let ws: WebSocket | undefined = undefined;
const screenshotRequests: Map<
string,
{
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => void;
rejecter: (reason?: any) => void;
}
> = new Map();
const assetRequests: Map<
string,
{
resolver: (value: void | PromiseLike<void>) => void;
rejecter: (reason?: any) => void;
}
> = new Map();
// Logger functions
const logger = {
debug: (...args: any[]) => {
if (logLevel === 'debug') console.log(...args);
},
info: (...args: any[]) => {
if (logLevel === 'info' || logLevel === 'debug') console.log(...args);
},
warn: (...args: any[]) => {
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug') console.warn(...args);
},
error: (...args: any[]) => {
if (logLevel !== 'none') console.error(...args);
},
};
return new Promise<InstanceClient>((resolveConnection, rejectConnection) => {
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
ws = new WebSocket(serverAddress);
ws.on('message', (data: Data) => {
let message: ServerMessage;
try {
message = JSON.parse(data.toString());
} catch (e) {
logger.error({ data, error: e }, 'Failed to parse JSON message');
return;
}
switch (message.type) {
case 'screenshot': {
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
logger.warn('Received invalid screenshot message:', message);
break;
}
const screenshotMessage = message as ScreenshotResponse;
const request = screenshotRequests.get(screenshotMessage.id);
if (!request) {
logger.warn(
`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`,
);
break;
}
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
request.resolver({ dataUri: screenshotMessage.dataUri });
screenshotRequests.delete(screenshotMessage.id);
break;
}
case 'screenshotError': {
if (!('message' in message) || !('id' in message)) {
logger.warn('Received invalid screenshot error message:', message);
break;
}
const errorMessage = message as ScreenshotErrorResponse;
const request = screenshotRequests.get(errorMessage.id);
if (!request) {
logger.warn(
`Received screenshot error for unknown or already handled session: ${errorMessage.id}`,
);
break;
}
logger.error(
`Server reported an error capturing screenshot for session ${errorMessage.id}:`,
errorMessage.message,
);
request.rejecter(new Error(errorMessage.message));
screenshotRequests.delete(errorMessage.id);
break;
}
case 'assetResult': {
logger.debug('Received assetResult:', message);
const request = assetRequests.get(message.url as string);
if (!request) {
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
break;
}
if (message.result === 'success') {
logger.debug('Asset result is success');
request.resolver();
assetRequests.delete(message.url as string);
break;
}
const errorMessage =
typeof message.message === 'string' && message.message ?
message.message
: `Asset processing failed: ${JSON.stringify(message)}`;
logger.debug('Asset result is failure', errorMessage);
request.rejecter(new Error(errorMessage));
assetRequests.delete(message.url as string);
break;
}
default:
logger.warn(`Received unexpected message type: ${message.type}`);
break;
}
});
ws.on('error', (err: Error) => {
logger.error('WebSocket error:', err.message);
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
rejectConnection(err);
}
screenshotRequests.forEach((request) => request.rejecter(err));
});
ws.on('close', () => {
logger.debug('Disconnected from server.');
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
});
const screenshot = async (): Promise<ScreenshotData> => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const id = 'ts-client-' + Date.now();
const screenshotRequest: ScreenshotRequest = {
type: 'screenshot',
id,
};
return new Promise<ScreenshotData>((resolve, reject) => {
logger.debug('Sending screenshot request:', screenshotRequest);
ws!.send(JSON.stringify(screenshotRequest), (err?: Error) => {
if (err) {
logger.error('Failed to send screenshot request:', err);
reject(err);
}
});
const timeout = setTimeout(() => {
if (screenshotRequests.has(id)) {
logger.error(`Screenshot request timed out for session ${id}`);
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
screenshotRequests.delete(id);
}
}, 30000);
screenshotRequests.set(id, {
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => {
clearTimeout(timeout);
resolve(value);
screenshotRequests.delete(id);
},
rejecter: (reason?: any) => {
clearTimeout(timeout);
reject(reason);
screenshotRequests.delete(id);
},
});
});
};
const disconnect = (): void => {
if (ws) {
logger.debug('Closing WebSocket connection.');
ws.close();
}
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
};
/**
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
* client to it.
*/
const startAdbTunnel = async (): Promise<Tunnel> => {
if (!androidInstance.status.adbWebSocketUrl) {
return Promise.reject(new Error('ADB WebSocket URL is not set'));
}
const { address, close } = await startTcpTunnel(
androidInstance.status.adbWebSocketUrl,
token,
'127.0.0.1',
0,
);
try {
await new Promise<void>((resolve, reject) => {
exec(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
if (err) return reject(err);
resolve();
});
});
logger.debug(`ADB connected on ${address.address}`);
} catch (err) {
close();
throw err;
}
return { address, close };
};
const sendAsset = async (url: string): Promise<void> => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
}
const assetRequest: AssetRequest = {
type: 'asset',
url,
};
ws.send(JSON.stringify(assetRequest), (err?: Error) => {
if (err) {
logger.error('Failed to send asset request:', err);
}
});
return new Promise<void>((resolve, reject) => {
assetRequests.set(url, { resolver: resolve, rejecter: reject });
});
};
ws.on('open', () => {
logger.debug(`Connected to ${serverAddress}`);
resolveConnection({
screenshot,
disconnect,
startAdbTunnel,
sendAsset,
});
});
});
}
import * as net from 'net';
import { WebSocket } from 'ws';
export interface Tunnel {
address: {
address: string;
port: number;
};
close: () => void;
}
/**
* Starts a one-shot TCP → WebSocket proxy.
*
* The function creates a local TCP server that listens on an ephemeral port on
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
* accepting further connections and forwards all traffic between that client
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
* more than one TCP connection, call `startTcpTunnel` again to create a new
* proxy instance.
*
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
* @param token Bearer token sent as `Authorization` header
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
* @param port Optional port number to listen on. Default is to ask Node.js
* to find an available non-privileged port.
*/
export async function startTcpTunnel(
remoteURL: string,
token: string,
hostname: string,
port: number,
): Promise<Tunnel> {
return new Promise((resolve, reject) => {
const server = net.createServer();
let ws: WebSocket | undefined;
let pingInterval: NodeJS.Timeout | undefined;
// close helper
const close = () => {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = undefined;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'close');
}
if (server.listening) {
server.close();
}
};
// No AbortController support – proxy can be closed via the returned handle
// TCP server error
server.once('error', (err) => {
close();
reject(new Error(`TCP server error: ${err.message}`));
});
// Listening
server.once('listening', () => {
const address = server.address();
if (!address || typeof address === 'string') {
close();
return reject(new Error('Failed to obtain listening address'));
}
resolve({ address, close });
});
// On first TCP connection
server.on('connection', (tcpSocket) => {
// Single-connection proxy
server.close();
ws = new WebSocket(remoteURL, {
headers: { Authorization: `Bearer ${token}` },
perMessageDeflate: false,
});
// WebSocket error
ws.once('error', (err: any) => {
console.error('WebSocket error:', err);
tcpSocket.destroy();
close();
});
ws.once('open', () => {
const socket = ws as WebSocket; // non-undefined after open
pingInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
(socket as any).ping();
}
}, 30_000);
// TCP → WS
tcpSocket.on('data', (chunk) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(chunk);
}
});
// WS → TCP
socket.on('message', (data: any) => {
if (!tcpSocket.destroyed) {
tcpSocket.write(data as Buffer);
}
});
});
// Mutual close
tcpSocket.on('close', close);
tcpSocket.on('error', (err: any) => {
console.error('TCP socket error:', err);
close();
});
ws.on('close', () => tcpSocket.destroy());
});
// Start listening
server.listen(port, hostname);
});
}