@limrun/api 0.5.2 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -112
- package/client.d.mts +6 -6
- package/client.d.mts.map +1 -1
- package/client.d.ts +6 -6
- package/client.d.ts.map +1 -1
- package/client.js +12 -6
- package/client.js.map +1 -1
- package/client.mjs +12 -6
- package/client.mjs.map +1 -1
- package/index.d.mts +0 -1
- package/index.d.ts +0 -1
- package/index.js +0 -2
- package/index.js.map +1 -1
- package/index.mjs +0 -1
- package/package.json +1 -4
- package/resources/android-instances-helpers.d.mts +51 -24
- package/resources/android-instances-helpers.d.mts.map +1 -1
- package/resources/android-instances-helpers.d.ts +51 -24
- package/resources/android-instances-helpers.d.ts.map +1 -1
- package/resources/android-instances-helpers.js +199 -107
- package/resources/android-instances-helpers.js.map +1 -1
- package/resources/android-instances-helpers.mjs +198 -103
- package/resources/android-instances-helpers.mjs.map +1 -1
- package/resources/android-instances.d.mts +4 -7
- package/resources/android-instances.d.mts.map +1 -1
- package/resources/android-instances.d.ts +4 -7
- package/resources/android-instances.d.ts.map +1 -1
- package/resources/android-instances.js.map +1 -1
- package/resources/android-instances.mjs.map +1 -1
- package/resources/index.d.mts +1 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/tunnel.d.mts +25 -0
- package/resources/tunnel.d.mts.map +1 -0
- package/resources/tunnel.d.ts +25 -0
- package/resources/tunnel.d.ts.map +1 -0
- package/resources/tunnel.js +102 -0
- package/resources/tunnel.js.map +1 -0
- package/resources/tunnel.mjs +98 -0
- package/resources/tunnel.mjs.map +1 -0
- package/src/client.ts +18 -14
- package/src/index.ts +0 -2
- package/src/resources/android-instances-helpers.ts +318 -127
- package/src/resources/android-instances.ts +3 -8
- package/src/resources/index.ts +0 -1
- package/src/resources/tunnel.ts +126 -0
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -1,159 +1,350 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
1
|
+
import { WebSocket, Data } from 'ws';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { startTcpTunnel } from './tunnel.js';
|
|
5
|
+
import type { Tunnel } from './tunnel.js';
|
|
6
|
+
export type { Tunnel } from './tunnel.js';
|
|
7
|
+
import { AndroidInstance } from './android-instances.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
9
|
-
* client to it.
|
|
10
|
+
* A client for interacting with a Limbar instance
|
|
10
11
|
*/
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
close();
|
|
34
|
-
throw err;
|
|
35
|
-
}
|
|
36
|
-
return { address, close };
|
|
12
|
+
export type InstanceClient = {
|
|
13
|
+
/**
|
|
14
|
+
* Take a screenshot of the current screen
|
|
15
|
+
* @returns A promise that resolves to the screenshot data
|
|
16
|
+
*/
|
|
17
|
+
screenshot: () => Promise<ScreenshotData>;
|
|
18
|
+
/**
|
|
19
|
+
* Disconnect from the Limbar instance
|
|
20
|
+
*/
|
|
21
|
+
disconnect: () => void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Establish an ADB tunnel to the instance.
|
|
25
|
+
* Returns the local TCP port and a cleanup function.
|
|
26
|
+
*/
|
|
27
|
+
startAdbTunnel: () => Promise<Tunnel>;
|
|
28
|
+
/**
|
|
29
|
+
* Send an asset URL to the instance. The instance will download the asset
|
|
30
|
+
* and process it (currently APK install is supported). Resolves on success,
|
|
31
|
+
* rejects with an Error on failure.
|
|
32
|
+
*/
|
|
33
|
+
sendAsset: (url: string) => Promise<void>;
|
|
37
34
|
};
|
|
38
35
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
36
|
+
/**
|
|
37
|
+
* Controls the verbosity of logging in the client
|
|
38
|
+
*/
|
|
39
|
+
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
44
40
|
|
|
45
41
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* The function creates a local TCP server that listens on an ephemeral port on
|
|
49
|
-
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
|
|
50
|
-
* accepting further connections and forwards all traffic between that client
|
|
51
|
-
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
|
|
52
|
-
* more than one TCP connection, call `startTcpProxy` again to create a new
|
|
53
|
-
* proxy instance.
|
|
54
|
-
*
|
|
55
|
-
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
|
|
56
|
-
* @param token Bearer token sent as `Authorization` header
|
|
57
|
-
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
|
|
58
|
-
* @param port Optional port number to listen on. Default is to ask Node.js
|
|
59
|
-
* to find an available non-privileged port.
|
|
42
|
+
* Configuration options for creating an Instance API client
|
|
60
43
|
*/
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// close helper
|
|
74
|
-
const close = () => {
|
|
75
|
-
if (pingInterval) {
|
|
76
|
-
clearInterval(pingInterval);
|
|
77
|
-
pingInterval = undefined;
|
|
78
|
-
}
|
|
79
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
80
|
-
ws.close(1000, 'close');
|
|
81
|
-
}
|
|
82
|
-
if (server.listening) {
|
|
83
|
-
server.close();
|
|
84
|
-
}
|
|
85
|
-
};
|
|
44
|
+
export type InstanceClientOptions = {
|
|
45
|
+
/**
|
|
46
|
+
* Path to the ADB executable.
|
|
47
|
+
* @default 'adb'
|
|
48
|
+
*/
|
|
49
|
+
adbPath?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Controls logging verbosity
|
|
52
|
+
* @default 'info'
|
|
53
|
+
*/
|
|
54
|
+
logLevel?: LogLevel;
|
|
55
|
+
};
|
|
86
56
|
|
|
87
|
-
|
|
57
|
+
type ScreenshotRequest = {
|
|
58
|
+
type: 'screenshot';
|
|
59
|
+
id: string;
|
|
60
|
+
};
|
|
88
61
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
type ScreenshotResponse = {
|
|
63
|
+
type: 'screenshot';
|
|
64
|
+
dataUri: string;
|
|
65
|
+
id: string;
|
|
66
|
+
};
|
|
94
67
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
68
|
+
type ScreenshotData = {
|
|
69
|
+
dataUri: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type ScreenshotErrorResponse = {
|
|
73
|
+
type: 'screenshotError';
|
|
74
|
+
message: string;
|
|
75
|
+
id: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type AssetRequest = {
|
|
79
|
+
type: 'asset';
|
|
80
|
+
url: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type AssetResultResponse = {
|
|
84
|
+
type: 'assetResult';
|
|
85
|
+
result: 'success' | 'failure' | string;
|
|
86
|
+
url: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type ServerMessage =
|
|
91
|
+
| ScreenshotResponse
|
|
92
|
+
| ScreenshotErrorResponse
|
|
93
|
+
| AssetResultResponse
|
|
94
|
+
| { type: string; [key: string]: unknown };
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a client for interacting with a Limbar instance
|
|
98
|
+
* @param options Configuration options including webrtcUrl, token and log level
|
|
99
|
+
* @returns An InstanceClient for controlling the instance
|
|
100
|
+
*/
|
|
101
|
+
export async function createInstanceClient(
|
|
102
|
+
androidInstance: AndroidInstance,
|
|
103
|
+
options: InstanceClientOptions = {
|
|
104
|
+
adbPath: 'adb',
|
|
105
|
+
logLevel: 'info',
|
|
106
|
+
},
|
|
107
|
+
): Promise<InstanceClient> {
|
|
108
|
+
const token = androidInstance.status.token;
|
|
109
|
+
const serverAddress = `${androidInstance.status.endpointWebSocketUrl}?token=${token}`;
|
|
110
|
+
const logLevel = options.logLevel ?? 'info';
|
|
111
|
+
let ws: WebSocket | undefined = undefined;
|
|
112
|
+
|
|
113
|
+
const screenshotRequests: Map<
|
|
114
|
+
string,
|
|
115
|
+
{
|
|
116
|
+
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => void;
|
|
117
|
+
rejecter: (reason?: any) => void;
|
|
118
|
+
}
|
|
119
|
+
> = new Map();
|
|
120
|
+
|
|
121
|
+
const assetRequests: Map<
|
|
122
|
+
string,
|
|
123
|
+
{
|
|
124
|
+
resolver: (value: void | PromiseLike<void>) => void;
|
|
125
|
+
rejecter: (reason?: any) => void;
|
|
126
|
+
}
|
|
127
|
+
> = new Map();
|
|
128
|
+
|
|
129
|
+
// Logger functions
|
|
130
|
+
const logger = {
|
|
131
|
+
debug: (...args: any[]) => {
|
|
132
|
+
if (logLevel === 'debug') console.log(...args);
|
|
133
|
+
},
|
|
134
|
+
info: (...args: any[]) => {
|
|
135
|
+
if (logLevel === 'info' || logLevel === 'debug') console.log(...args);
|
|
136
|
+
},
|
|
137
|
+
warn: (...args: any[]) => {
|
|
138
|
+
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug') console.warn(...args);
|
|
139
|
+
},
|
|
140
|
+
error: (...args: any[]) => {
|
|
141
|
+
if (logLevel !== 'none') console.error(...args);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return new Promise<InstanceClient>((resolveConnection, rejectConnection) => {
|
|
146
|
+
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
|
|
147
|
+
ws = new WebSocket(serverAddress);
|
|
148
|
+
ws.on('message', (data: Data) => {
|
|
149
|
+
let message: ServerMessage;
|
|
150
|
+
try {
|
|
151
|
+
message = JSON.parse(data.toString());
|
|
152
|
+
} catch (e) {
|
|
153
|
+
logger.error({ data, error: e }, 'Failed to parse JSON message');
|
|
154
|
+
return;
|
|
101
155
|
}
|
|
102
|
-
resolve({ address, close });
|
|
103
|
-
});
|
|
104
156
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
157
|
+
switch (message.type) {
|
|
158
|
+
case 'screenshot': {
|
|
159
|
+
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
|
|
160
|
+
logger.warn('Received invalid screenshot message:', message);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const screenshotMessage = message as ScreenshotResponse;
|
|
165
|
+
const request = screenshotRequests.get(screenshotMessage.id);
|
|
109
166
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
167
|
+
if (!request) {
|
|
168
|
+
logger.warn(
|
|
169
|
+
`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`,
|
|
170
|
+
);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
114
173
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
174
|
+
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
|
|
175
|
+
request.resolver({ dataUri: screenshotMessage.dataUri });
|
|
176
|
+
screenshotRequests.delete(screenshotMessage.id);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'screenshotError': {
|
|
180
|
+
if (!('message' in message) || !('id' in message)) {
|
|
181
|
+
logger.warn('Received invalid screenshot error message:', message);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const errorMessage = message as ScreenshotErrorResponse;
|
|
186
|
+
const request = screenshotRequests.get(errorMessage.id);
|
|
121
187
|
|
|
122
|
-
|
|
123
|
-
|
|
188
|
+
if (!request) {
|
|
189
|
+
logger.warn(
|
|
190
|
+
`Received screenshot error for unknown or already handled session: ${errorMessage.id}`,
|
|
191
|
+
);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
124
194
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
195
|
+
logger.error(
|
|
196
|
+
`Server reported an error capturing screenshot for session ${errorMessage.id}:`,
|
|
197
|
+
errorMessage.message,
|
|
198
|
+
);
|
|
199
|
+
request.rejecter(new Error(errorMessage.message));
|
|
200
|
+
screenshotRequests.delete(errorMessage.id);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'assetResult': {
|
|
204
|
+
logger.debug('Received assetResult:', message);
|
|
205
|
+
const request = assetRequests.get(message.url as string);
|
|
206
|
+
if (!request) {
|
|
207
|
+
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (message.result === 'success') {
|
|
211
|
+
logger.debug('Asset result is success');
|
|
212
|
+
request.resolver();
|
|
213
|
+
assetRequests.delete(message.url as string);
|
|
214
|
+
break;
|
|
128
215
|
}
|
|
129
|
-
|
|
216
|
+
const errorMessage =
|
|
217
|
+
typeof message.message === 'string' && message.message ?
|
|
218
|
+
message.message
|
|
219
|
+
: `Asset processing failed: ${JSON.stringify(message)}`;
|
|
220
|
+
logger.debug('Asset result is failure', errorMessage);
|
|
221
|
+
request.rejecter(new Error(errorMessage));
|
|
222
|
+
assetRequests.delete(message.url as string);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
default:
|
|
226
|
+
logger.warn(`Received unexpected message type: ${message.type}`);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
ws.on('error', (err: Error) => {
|
|
232
|
+
logger.error('WebSocket error:', err.message);
|
|
233
|
+
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
|
|
234
|
+
rejectConnection(err);
|
|
235
|
+
}
|
|
236
|
+
screenshotRequests.forEach((request) => request.rejecter(err));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
ws.on('close', () => {
|
|
240
|
+
logger.debug('Disconnected from server.');
|
|
241
|
+
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const screenshot = async (): Promise<ScreenshotData> => {
|
|
245
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
246
|
+
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const id = 'ts-client-' + Date.now();
|
|
250
|
+
const screenshotRequest: ScreenshotRequest = {
|
|
251
|
+
type: 'screenshot',
|
|
252
|
+
id,
|
|
253
|
+
};
|
|
130
254
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
255
|
+
return new Promise<ScreenshotData>((resolve, reject) => {
|
|
256
|
+
logger.debug('Sending screenshot request:', screenshotRequest);
|
|
257
|
+
ws!.send(JSON.stringify(screenshotRequest), (err?: Error) => {
|
|
258
|
+
if (err) {
|
|
259
|
+
logger.error('Failed to send screenshot request:', err);
|
|
260
|
+
reject(err);
|
|
135
261
|
}
|
|
136
262
|
});
|
|
137
263
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
264
|
+
const timeout = setTimeout(() => {
|
|
265
|
+
if (screenshotRequests.has(id)) {
|
|
266
|
+
logger.error(`Screenshot request timed out for session ${id}`);
|
|
267
|
+
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
|
|
268
|
+
screenshotRequests.delete(id);
|
|
142
269
|
}
|
|
270
|
+
}, 30000);
|
|
271
|
+
screenshotRequests.set(id, {
|
|
272
|
+
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => {
|
|
273
|
+
clearTimeout(timeout);
|
|
274
|
+
resolve(value);
|
|
275
|
+
screenshotRequests.delete(id);
|
|
276
|
+
},
|
|
277
|
+
rejecter: (reason?: any) => {
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
reject(reason);
|
|
280
|
+
screenshotRequests.delete(id);
|
|
281
|
+
},
|
|
143
282
|
});
|
|
144
283
|
});
|
|
284
|
+
};
|
|
145
285
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
286
|
+
const disconnect = (): void => {
|
|
287
|
+
if (ws) {
|
|
288
|
+
logger.debug('Closing WebSocket connection.');
|
|
289
|
+
ws.close();
|
|
290
|
+
}
|
|
291
|
+
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
|
|
296
|
+
* client to it.
|
|
297
|
+
*/
|
|
298
|
+
const startAdbTunnel = async (): Promise<Tunnel> => {
|
|
299
|
+
if (!androidInstance.status.adbWebSocketUrl) {
|
|
300
|
+
return Promise.reject(new Error('ADB WebSocket URL is not set'));
|
|
301
|
+
}
|
|
302
|
+
const { address, close } = await startTcpTunnel(
|
|
303
|
+
androidInstance.status.adbWebSocketUrl,
|
|
304
|
+
token,
|
|
305
|
+
'127.0.0.1',
|
|
306
|
+
0,
|
|
307
|
+
);
|
|
308
|
+
try {
|
|
309
|
+
await new Promise<void>((resolve, reject) => {
|
|
310
|
+
exec(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
|
|
311
|
+
if (err) return reject(err);
|
|
312
|
+
resolve();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
logger.debug(`ADB connected on ${address.address}`);
|
|
316
|
+
} catch (err) {
|
|
150
317
|
close();
|
|
151
|
-
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
return { address, close };
|
|
321
|
+
};
|
|
152
322
|
|
|
153
|
-
|
|
323
|
+
const sendAsset = async (url: string): Promise<void> => {
|
|
324
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
325
|
+
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
|
|
326
|
+
}
|
|
327
|
+
const assetRequest: AssetRequest = {
|
|
328
|
+
type: 'asset',
|
|
329
|
+
url,
|
|
330
|
+
};
|
|
331
|
+
ws.send(JSON.stringify(assetRequest), (err?: Error) => {
|
|
332
|
+
if (err) {
|
|
333
|
+
logger.error('Failed to send asset request:', err);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
return new Promise<void>((resolve, reject) => {
|
|
337
|
+
assetRequests.set(url, { resolver: resolve, rejecter: reject });
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
ws.on('open', () => {
|
|
341
|
+
logger.debug(`Connected to ${serverAddress}`);
|
|
342
|
+
resolveConnection({
|
|
343
|
+
screenshot,
|
|
344
|
+
disconnect,
|
|
345
|
+
startAdbTunnel,
|
|
346
|
+
sendAsset,
|
|
347
|
+
});
|
|
154
348
|
});
|
|
155
|
-
|
|
156
|
-
// Start listening
|
|
157
|
-
server.listen(port, hostname);
|
|
158
349
|
});
|
|
159
350
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
2
|
|
|
3
3
|
import { APIResource } from '../core/resource';
|
|
4
|
-
import * as AndroidInstancesAPI from './android-instances';
|
|
5
4
|
import { APIPromise } from '../core/api-promise';
|
|
6
5
|
import { buildHeaders } from '../internal/headers';
|
|
7
6
|
import { RequestOptions } from '../internal/request-options';
|
|
@@ -77,8 +76,7 @@ export namespace AndroidInstance {
|
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
78
|
* The region where the instance will be created. If not given, will be decided
|
|
80
|
-
* based on
|
|
81
|
-
* availability.
|
|
79
|
+
* based on scheduling clues and availability.
|
|
82
80
|
*/
|
|
83
81
|
region: string;
|
|
84
82
|
|
|
@@ -92,7 +90,7 @@ export namespace AndroidInstance {
|
|
|
92
90
|
export interface Status {
|
|
93
91
|
token: string;
|
|
94
92
|
|
|
95
|
-
state:
|
|
93
|
+
state: 'unknown' | 'creating' | 'ready' | 'terminated';
|
|
96
94
|
|
|
97
95
|
adbWebSocketUrl?: string;
|
|
98
96
|
|
|
@@ -100,8 +98,6 @@ export namespace AndroidInstance {
|
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
export type AndroidInstanceState = 'unknown' | 'creating' | 'ready' | 'terminated';
|
|
104
|
-
|
|
105
101
|
export type AndroidInstanceListResponse = Array<AndroidInstance>;
|
|
106
102
|
|
|
107
103
|
export interface AndroidInstanceCreateParams {
|
|
@@ -187,13 +183,12 @@ export interface AndroidInstanceListParams {
|
|
|
187
183
|
/**
|
|
188
184
|
* State filter to apply to Android instances to return.
|
|
189
185
|
*/
|
|
190
|
-
state?:
|
|
186
|
+
state?: 'unknown' | 'creating' | 'ready' | 'terminated';
|
|
191
187
|
}
|
|
192
188
|
|
|
193
189
|
export declare namespace AndroidInstances {
|
|
194
190
|
export {
|
|
195
191
|
type AndroidInstance as AndroidInstance,
|
|
196
|
-
type AndroidInstanceState as AndroidInstanceState,
|
|
197
192
|
type AndroidInstanceListResponse as AndroidInstanceListResponse,
|
|
198
193
|
type AndroidInstanceCreateParams as AndroidInstanceCreateParams,
|
|
199
194
|
type AndroidInstanceListParams as AndroidInstanceListParams,
|
package/src/resources/index.ts
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
|
|
4
|
+
export interface Tunnel {
|
|
5
|
+
address: {
|
|
6
|
+
address: string;
|
|
7
|
+
port: number;
|
|
8
|
+
};
|
|
9
|
+
close: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Starts a one-shot TCP → WebSocket proxy.
|
|
14
|
+
*
|
|
15
|
+
* The function creates a local TCP server that listens on an ephemeral port on
|
|
16
|
+
* 127.0.0.1. As soon as the **first** TCP client connects the server stops
|
|
17
|
+
* accepting further connections and forwards all traffic between that client
|
|
18
|
+
* and `remoteURL` through an authenticated WebSocket. If you need to proxy
|
|
19
|
+
* more than one TCP connection, call `startTcpTunnel` again to create a new
|
|
20
|
+
* proxy instance.
|
|
21
|
+
*
|
|
22
|
+
* @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
|
|
23
|
+
* @param token Bearer token sent as `Authorization` header
|
|
24
|
+
* @param hostname Optional IP address to listen on. Default is 127.0.0.1
|
|
25
|
+
* @param port Optional port number to listen on. Default is to ask Node.js
|
|
26
|
+
* to find an available non-privileged port.
|
|
27
|
+
*/
|
|
28
|
+
export async function startTcpTunnel(
|
|
29
|
+
remoteURL: string,
|
|
30
|
+
token: string,
|
|
31
|
+
hostname: string,
|
|
32
|
+
port: number,
|
|
33
|
+
): Promise<Tunnel> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const server = net.createServer();
|
|
36
|
+
|
|
37
|
+
let ws: WebSocket | undefined;
|
|
38
|
+
let pingInterval: NodeJS.Timeout | undefined;
|
|
39
|
+
|
|
40
|
+
// close helper
|
|
41
|
+
const close = () => {
|
|
42
|
+
if (pingInterval) {
|
|
43
|
+
clearInterval(pingInterval);
|
|
44
|
+
pingInterval = undefined;
|
|
45
|
+
}
|
|
46
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
47
|
+
ws.close(1000, 'close');
|
|
48
|
+
}
|
|
49
|
+
if (server.listening) {
|
|
50
|
+
server.close();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// No AbortController support – proxy can be closed via the returned handle
|
|
55
|
+
|
|
56
|
+
// TCP server error
|
|
57
|
+
server.once('error', (err) => {
|
|
58
|
+
close();
|
|
59
|
+
reject(new Error(`TCP server error: ${err.message}`));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Listening
|
|
63
|
+
server.once('listening', () => {
|
|
64
|
+
const address = server.address();
|
|
65
|
+
if (!address || typeof address === 'string') {
|
|
66
|
+
close();
|
|
67
|
+
return reject(new Error('Failed to obtain listening address'));
|
|
68
|
+
}
|
|
69
|
+
resolve({ address, close });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// On first TCP connection
|
|
73
|
+
server.on('connection', (tcpSocket) => {
|
|
74
|
+
// Single-connection proxy
|
|
75
|
+
server.close();
|
|
76
|
+
|
|
77
|
+
ws = new WebSocket(remoteURL, {
|
|
78
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
79
|
+
perMessageDeflate: false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// WebSocket error
|
|
83
|
+
ws.once('error', (err: any) => {
|
|
84
|
+
console.error('WebSocket error:', err);
|
|
85
|
+
tcpSocket.destroy();
|
|
86
|
+
close();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
ws.once('open', () => {
|
|
90
|
+
const socket = ws as WebSocket; // non-undefined after open
|
|
91
|
+
|
|
92
|
+
pingInterval = setInterval(() => {
|
|
93
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
94
|
+
(socket as any).ping();
|
|
95
|
+
}
|
|
96
|
+
}, 30_000);
|
|
97
|
+
|
|
98
|
+
// TCP → WS
|
|
99
|
+
tcpSocket.on('data', (chunk) => {
|
|
100
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
101
|
+
socket.send(chunk);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// WS → TCP
|
|
106
|
+
socket.on('message', (data: any) => {
|
|
107
|
+
if (!tcpSocket.destroyed) {
|
|
108
|
+
tcpSocket.write(data as Buffer);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Mutual close
|
|
114
|
+
tcpSocket.on('close', close);
|
|
115
|
+
tcpSocket.on('error', (err: any) => {
|
|
116
|
+
console.error('TCP socket error:', err);
|
|
117
|
+
close();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
ws.on('close', () => tcpSocket.destroy());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Start listening
|
|
124
|
+
server.listen(port, hostname);
|
|
125
|
+
});
|
|
126
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.6.2'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.6.2";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.6.2";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED