@limrun/api 0.23.2 → 0.24.0
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 +16 -0
- package/client.d.mts +3 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts +3 -0
- package/client.d.ts.map +1 -1
- package/client.js +3 -0
- package/client.js.map +1 -1
- package/client.mjs +3 -0
- package/client.mjs.map +1 -1
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js +4 -2
- package/exec-client.js.map +1 -1
- package/exec-client.mjs +4 -2
- package/exec-client.mjs.map +1 -1
- package/folder-sync.d.mts.map +1 -1
- package/folder-sync.d.ts.map +1 -1
- package/folder-sync.js +5 -2
- package/folder-sync.js.map +1 -1
- package/folder-sync.mjs +5 -2
- package/folder-sync.mjs.map +1 -1
- package/instance-client.d.mts +19 -4
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +19 -4
- package/instance-client.d.ts.map +1 -1
- package/instance-client.js +42 -2
- package/instance-client.js.map +1 -1
- package/instance-client.mjs +42 -2
- package/instance-client.mjs.map +1 -1
- package/internal/download-file.d.mts +2 -0
- package/internal/download-file.d.mts.map +1 -0
- package/internal/download-file.d.ts +2 -0
- package/internal/download-file.d.ts.map +1 -0
- package/internal/download-file.js +35 -0
- package/internal/download-file.js.map +1 -0
- package/internal/download-file.mjs +31 -0
- package/internal/download-file.mjs.map +1 -0
- package/internal/proxy-transport.d.mts +14 -0
- package/internal/proxy-transport.d.mts.map +1 -0
- package/internal/proxy-transport.d.ts +14 -0
- package/internal/proxy-transport.d.ts.map +1 -0
- package/internal/proxy-transport.js +59 -0
- package/internal/proxy-transport.js.map +1 -0
- package/internal/proxy-transport.mjs +56 -0
- package/internal/proxy-transport.mjs.map +1 -0
- package/internal/shims.d.mts.map +1 -1
- package/internal/shims.d.ts.map +1 -1
- package/internal/shims.js +2 -1
- package/internal/shims.js.map +1 -1
- package/internal/shims.mjs +2 -1
- package/internal/shims.mjs.map +1 -1
- package/internal/utils/env.js +2 -2
- package/internal/utils/env.js.map +1 -1
- package/internal/utils/env.mjs +2 -2
- package/internal/utils/env.mjs.map +1 -1
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +11 -34
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +10 -33
- package/ios-client.mjs.map +1 -1
- package/package.json +4 -1
- package/resources/android-instances.d.mts +5 -4
- package/resources/android-instances.d.mts.map +1 -1
- package/resources/android-instances.d.ts +5 -4
- package/resources/android-instances.d.ts.map +1 -1
- package/resources/assets-helpers.d.mts.map +1 -1
- package/resources/assets-helpers.d.ts.map +1 -1
- package/resources/assets-helpers.js +2 -1
- package/resources/assets-helpers.js.map +1 -1
- package/resources/assets-helpers.mjs +2 -1
- package/resources/assets-helpers.mjs.map +1 -1
- package/resources/index.d.mts +1 -0
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -0
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +3 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -0
- package/resources/index.mjs.map +1 -1
- package/resources/ios-instances.d.mts +4 -4
- package/resources/ios-instances.d.ts +4 -4
- package/resources/xcode-instances.d.mts +122 -0
- package/resources/xcode-instances.d.mts.map +1 -0
- package/resources/xcode-instances.d.ts +122 -0
- package/resources/xcode-instances.d.ts.map +1 -0
- package/resources/xcode-instances.js +40 -0
- package/resources/xcode-instances.js.map +1 -0
- package/resources/xcode-instances.mjs +36 -0
- package/resources/xcode-instances.mjs.map +1 -0
- package/sandbox-client.d.mts.map +1 -1
- package/sandbox-client.d.ts.map +1 -1
- package/sandbox-client.js +2 -1
- package/sandbox-client.js.map +1 -1
- package/sandbox-client.mjs +2 -1
- package/sandbox-client.mjs.map +1 -1
- package/src/client.ts +17 -0
- package/src/exec-client.ts +4 -2
- package/src/folder-sync.ts +15 -12
- package/src/instance-client.ts +86 -8
- package/src/internal/download-file.ts +33 -0
- package/src/internal/proxy-transport.ts +69 -0
- package/src/internal/shims.ts +2 -1
- package/src/internal/utils/env.ts +2 -2
- package/src/ios-client.ts +10 -35
- package/src/resources/android-instances.ts +6 -4
- package/src/resources/assets-helpers.ts +2 -1
- package/src/resources/index.ts +7 -0
- package/src/resources/ios-instances.ts +4 -4
- package/src/resources/xcode-instances.ts +177 -0
- package/src/sandbox-client.ts +2 -1
- package/src/tunnel.ts +5 -0
- package/src/version.ts +1 -1
- package/tunnel.d.mts.map +1 -1
- package/tunnel.d.ts.map +1 -1
- package/tunnel.js +5 -0
- package/tunnel.js.map +1 -1
- package/tunnel.mjs +5 -0
- package/tunnel.mjs.map +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/exec-client.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { createEventSource, type EventSourceClient, type EventSourceMessage } from 'eventsource-client';
|
|
9
|
+
import { nodeProxyTransport } from './internal/proxy-transport';
|
|
9
10
|
|
|
10
11
|
// =============================================================================
|
|
11
12
|
// Types
|
|
@@ -153,7 +154,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
155
156
|
try {
|
|
156
|
-
await fetch(`${this.options.apiUrl}/exec/${this.execId}/cancel`, {
|
|
157
|
+
await nodeProxyTransport.fetch(`${this.options.apiUrl}/exec/${this.execId}/cancel`, {
|
|
157
158
|
method: 'POST',
|
|
158
159
|
headers: {
|
|
159
160
|
Authorization: `Bearer ${this.options.token}`,
|
|
@@ -172,7 +173,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
|
172
173
|
// 1. Trigger the build via POST /exec
|
|
173
174
|
let execRes: Response;
|
|
174
175
|
try {
|
|
175
|
-
execRes = await fetch(`${apiUrl}/exec`, {
|
|
176
|
+
execRes = await nodeProxyTransport.fetch(`${apiUrl}/exec`, {
|
|
176
177
|
method: 'POST',
|
|
177
178
|
headers: {
|
|
178
179
|
'Content-Type': 'application/json',
|
|
@@ -273,6 +274,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
|
273
274
|
try {
|
|
274
275
|
const eventSource = createEventSource({
|
|
275
276
|
url: eventsUrl,
|
|
277
|
+
fetch: nodeProxyTransport.fetch,
|
|
276
278
|
headers: { Authorization: `Bearer ${this.options.token}` },
|
|
277
279
|
onMessage: (message: EventSourceMessage) => {
|
|
278
280
|
const data = typeof message.data === 'string' ? message.data : String(message.data ?? '');
|
package/src/folder-sync.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { watchFolderTree } from './folder-sync-watcher';
|
|
|
7
7
|
import { type IgnoreFn } from './folder-sync-ignore';
|
|
8
8
|
import { Readable } from 'stream';
|
|
9
9
|
import * as zlib from 'zlib';
|
|
10
|
+
import { nodeProxyTransport } from './internal/proxy-transport';
|
|
10
11
|
|
|
11
12
|
// =============================================================================
|
|
12
13
|
// Folder Sync (HTTP batch)
|
|
@@ -218,18 +219,20 @@ async function httpFolderSyncBatch(
|
|
|
218
219
|
};
|
|
219
220
|
sourceStream.on('error', onStreamError);
|
|
220
221
|
bodyStream.on('error', onStreamError);
|
|
221
|
-
const res = await
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
222
|
+
const res = await nodeProxyTransport
|
|
223
|
+
.fetch(url, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers,
|
|
226
|
+
body: bodyStream as any,
|
|
227
|
+
duplex: 'half' as any,
|
|
228
|
+
signal: controller.signal,
|
|
229
|
+
} as any)
|
|
230
|
+
.catch((err) => {
|
|
231
|
+
if (streamError) {
|
|
232
|
+
throw streamError;
|
|
233
|
+
}
|
|
234
|
+
throw err;
|
|
235
|
+
});
|
|
233
236
|
const text = await res.text();
|
|
234
237
|
if (!res.ok) {
|
|
235
238
|
throw new Error(`folder-sync http failed: ${res.status} ${text}`);
|
package/src/instance-client.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { WebSocket, Data } from 'ws';
|
|
2
2
|
import { exec } from 'node:child_process';
|
|
3
3
|
|
|
4
|
+
import { downloadFileToLocalPath } from './internal/download-file';
|
|
5
|
+
import { nodeProxyTransport } from './internal/proxy-transport';
|
|
4
6
|
import { startTcpTunnel, isNonRetryableError } from './tunnel';
|
|
5
7
|
import type { Tunnel } from './tunnel';
|
|
6
8
|
|
|
9
|
+
const ANDROID_RECORDING_PATH = '/data/local/tmp/recordings/video_recording.mp4';
|
|
10
|
+
const ANDROID_SIGNALING_PATH = '/ws';
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Connection state of the instance client
|
|
9
14
|
*/
|
|
@@ -14,6 +19,19 @@ export type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'rec
|
|
|
14
19
|
*/
|
|
15
20
|
export type ConnectionStateCallback = (state: ConnectionState) => void;
|
|
16
21
|
|
|
22
|
+
function deriveEndpointWebSocketUrl(apiUrl: string): string {
|
|
23
|
+
const parsed = new URL(apiUrl);
|
|
24
|
+
parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
25
|
+
parsed.search = '';
|
|
26
|
+
parsed.hash = '';
|
|
27
|
+
parsed.pathname = `${parsed.pathname.replace(/\/$/, '')}${ANDROID_SIGNALING_PATH}`;
|
|
28
|
+
return parsed.toString().replace(/\/$/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildDownloadUrl(apiUrl: string): string {
|
|
32
|
+
return `${apiUrl}/files?path=${encodeURIComponent(ANDROID_RECORDING_PATH)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* A client for interacting with a Limbar instance
|
|
19
37
|
*/
|
|
@@ -64,6 +82,17 @@ export type InstanceClient = {
|
|
|
64
82
|
* Open a URL/deeplink on Android.
|
|
65
83
|
*/
|
|
66
84
|
openUrl: (url: string) => Promise<OpenUrlResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Start recording device video. Use stopRecording() to finish the recording.
|
|
87
|
+
*/
|
|
88
|
+
startRecording: () => Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Stop the active server-side recording.
|
|
91
|
+
* If `saveTo.presignedUrl` is provided, the server uploads the completed file there before resolving.
|
|
92
|
+
* If `saveTo.localPath` is provided, the client downloads the completed file to that path.
|
|
93
|
+
* Returns a download URL for the completed recording.
|
|
94
|
+
*/
|
|
95
|
+
stopRecording: (saveTo: { presignedUrl?: string; localPath?: string }) => Promise<string>;
|
|
67
96
|
/**
|
|
68
97
|
* Disconnect from the Limbar instance
|
|
69
98
|
*/
|
|
@@ -151,13 +180,14 @@ export type AndroidElementNode = {
|
|
|
151
180
|
*/
|
|
152
181
|
export type InstanceClientOptions = {
|
|
153
182
|
/**
|
|
154
|
-
*
|
|
183
|
+
* HTTP base URL for the Android daemon. WebSocket control is derived from it
|
|
184
|
+
* using the `/ws` path, and recording downloads use the same base URL.
|
|
155
185
|
*/
|
|
156
|
-
|
|
186
|
+
apiUrl: string;
|
|
157
187
|
/**
|
|
158
|
-
* The URL of the
|
|
188
|
+
* The URL of the ADB WebSocket endpoint.
|
|
159
189
|
*/
|
|
160
|
-
|
|
190
|
+
adbUrl?: string;
|
|
161
191
|
/**
|
|
162
192
|
* The token to use for the WebSocket connections.
|
|
163
193
|
*/
|
|
@@ -234,6 +264,8 @@ export type OpenUrlResult = {
|
|
|
234
264
|
url: string;
|
|
235
265
|
};
|
|
236
266
|
|
|
267
|
+
type EmptyCommandResult = Record<string, never>;
|
|
268
|
+
|
|
237
269
|
type ScreenshotErrorResponse = {
|
|
238
270
|
type: 'screenshotError';
|
|
239
271
|
message: string;
|
|
@@ -321,6 +353,20 @@ type OpenUrlResultMessage = {
|
|
|
321
353
|
error?: CommandError;
|
|
322
354
|
};
|
|
323
355
|
|
|
356
|
+
type StartVideoRecordingResultMessage = {
|
|
357
|
+
type: 'startRecordingResult';
|
|
358
|
+
id: string;
|
|
359
|
+
payload?: EmptyCommandResult;
|
|
360
|
+
error?: CommandError;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
type StopVideoRecordingResultMessage = {
|
|
364
|
+
type: 'stopRecordingResult';
|
|
365
|
+
id: string;
|
|
366
|
+
payload?: EmptyCommandResult;
|
|
367
|
+
error?: CommandError;
|
|
368
|
+
};
|
|
369
|
+
|
|
324
370
|
type KnownCommandResultMessage =
|
|
325
371
|
| ScreenshotResultMessage
|
|
326
372
|
| GetElementTreeResultMessage
|
|
@@ -330,7 +376,9 @@ type KnownCommandResultMessage =
|
|
|
330
376
|
| PressKeyResultMessage
|
|
331
377
|
| ScrollScreenResultMessage
|
|
332
378
|
| ScrollElementResultMessage
|
|
333
|
-
| OpenUrlResultMessage
|
|
379
|
+
| OpenUrlResultMessage
|
|
380
|
+
| StartVideoRecordingResultMessage
|
|
381
|
+
| StopVideoRecordingResultMessage;
|
|
334
382
|
|
|
335
383
|
type ServerMessage =
|
|
336
384
|
| ScreenshotResponse
|
|
@@ -349,6 +397,8 @@ type CommandRequestMap = {
|
|
|
349
397
|
scrollScreen: { direction: ScrollDirection; amount?: number };
|
|
350
398
|
scrollElement: AndroidElementTarget & { direction: ScrollDirection; amount?: number };
|
|
351
399
|
openUrl: { url: string };
|
|
400
|
+
startRecording: Record<string, never>;
|
|
401
|
+
stopRecording: { upload?: { presignedUrl: string } };
|
|
352
402
|
};
|
|
353
403
|
|
|
354
404
|
type CommandResultMap = {
|
|
@@ -361,6 +411,8 @@ type CommandResultMap = {
|
|
|
361
411
|
scrollScreen: ScrollResult;
|
|
362
412
|
scrollElement: ScrollResult;
|
|
363
413
|
openUrl: OpenUrlResult;
|
|
414
|
+
startRecording: EmptyCommandResult;
|
|
415
|
+
stopRecording: EmptyCommandResult;
|
|
364
416
|
};
|
|
365
417
|
|
|
366
418
|
type PendingRequest<T> = {
|
|
@@ -375,7 +427,9 @@ type PendingRequest<T> = {
|
|
|
375
427
|
* @returns An InstanceClient for controlling the instance
|
|
376
428
|
*/
|
|
377
429
|
export async function createInstanceClient(options: InstanceClientOptions): Promise<InstanceClient> {
|
|
378
|
-
const
|
|
430
|
+
const endpointWebSocketUrl = deriveEndpointWebSocketUrl(options.apiUrl);
|
|
431
|
+
const serverAddress = `${endpointWebSocketUrl}?token=${options.token}`;
|
|
432
|
+
const recordingApiUrl = options.apiUrl;
|
|
379
433
|
const logLevel = options.logLevel ?? 'info';
|
|
380
434
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? 6;
|
|
381
435
|
const reconnectDelay = options.reconnectDelay ?? 1000;
|
|
@@ -387,7 +441,6 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
387
441
|
let reconnectTimeout: NodeJS.Timeout | undefined;
|
|
388
442
|
let intentionalDisconnect = false;
|
|
389
443
|
let lastError: string | undefined;
|
|
390
|
-
|
|
391
444
|
const pendingRequests: Map<string, PendingRequest<unknown>> = new Map();
|
|
392
445
|
const pendingAssetRequestsByUrl: Map<string, Array<PendingRequest<void>>> = new Map();
|
|
393
446
|
|
|
@@ -521,6 +574,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
521
574
|
case 'scrollScreenResult':
|
|
522
575
|
case 'scrollElementResult':
|
|
523
576
|
case 'openUrlResult':
|
|
577
|
+
case 'startRecordingResult':
|
|
578
|
+
case 'stopRecordingResult':
|
|
524
579
|
return 'id' in message && typeof message.id === 'string';
|
|
525
580
|
default:
|
|
526
581
|
return false;
|
|
@@ -597,7 +652,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
597
652
|
cleanup();
|
|
598
653
|
updateConnectionState('connecting');
|
|
599
654
|
|
|
600
|
-
|
|
655
|
+
const proxyAgent = nodeProxyTransport.getWebSocketAgent(serverAddress);
|
|
656
|
+
ws = new WebSocket(serverAddress, proxyAgent ? { agent: proxyAgent } : {});
|
|
601
657
|
|
|
602
658
|
ws.on('message', (data: Data) => {
|
|
603
659
|
let message: ServerMessage;
|
|
@@ -757,6 +813,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
757
813
|
scrollScreen,
|
|
758
814
|
scrollElement,
|
|
759
815
|
openUrl,
|
|
816
|
+
startRecording,
|
|
817
|
+
stopRecording,
|
|
760
818
|
disconnect,
|
|
761
819
|
startAdbTunnel,
|
|
762
820
|
sendAsset,
|
|
@@ -846,6 +904,23 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
846
904
|
};
|
|
847
905
|
};
|
|
848
906
|
|
|
907
|
+
const startRecording = async (): Promise<void> => {
|
|
908
|
+
await sendRequest('startRecording', {});
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const stopRecording = async (saveTo: { presignedUrl?: string; localPath?: string }): Promise<string> => {
|
|
912
|
+
const request: CommandRequestMap['stopRecording'] = {};
|
|
913
|
+
if (saveTo.presignedUrl) {
|
|
914
|
+
request.upload = { presignedUrl: saveTo.presignedUrl };
|
|
915
|
+
}
|
|
916
|
+
await sendRequest('stopRecording', request);
|
|
917
|
+
const downloadUrl = buildDownloadUrl(recordingApiUrl);
|
|
918
|
+
if (saveTo.localPath) {
|
|
919
|
+
await downloadFileToLocalPath(downloadUrl, options.token, saveTo.localPath);
|
|
920
|
+
}
|
|
921
|
+
return downloadUrl;
|
|
922
|
+
};
|
|
923
|
+
|
|
849
924
|
const disconnect = (): void => {
|
|
850
925
|
intentionalDisconnect = true;
|
|
851
926
|
cleanup();
|
|
@@ -870,6 +945,9 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
870
945
|
* client to it.
|
|
871
946
|
*/
|
|
872
947
|
const startAdbTunnel = async (): Promise<Tunnel> => {
|
|
948
|
+
if (!options.adbUrl) {
|
|
949
|
+
throw new Error('adbUrl is required to start an ADB tunnel.');
|
|
950
|
+
}
|
|
873
951
|
const tunnel = await startTcpTunnel(options.adbUrl, options.token, '127.0.0.1', 0, {
|
|
874
952
|
maxReconnectAttempts,
|
|
875
953
|
reconnectDelay,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
5
|
+
|
|
6
|
+
import { nodeProxyTransport } from './proxy-transport';
|
|
7
|
+
|
|
8
|
+
export async function downloadFileToLocalPath(url: string, token: string, localPath: string): Promise<void> {
|
|
9
|
+
const maxRetries = 3;
|
|
10
|
+
|
|
11
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
12
|
+
const response = await nodeProxyTransport.fetch(url, {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${token}`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
const errorBody = await response.text();
|
|
20
|
+
const isRetriable = response.status >= 500 && response.status < 600;
|
|
21
|
+
if (isRetriable && attempt < maxRetries) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Download failed: ${response.status} ${errorBody}`);
|
|
25
|
+
}
|
|
26
|
+
if (!response.body) {
|
|
27
|
+
throw new Error('Download failed: response body is missing');
|
|
28
|
+
}
|
|
29
|
+
await fs.promises.mkdir(path.dirname(localPath), { recursive: true });
|
|
30
|
+
await pipeline(Readable.fromWeb(response.body as any), fs.createWriteStream(localPath));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Agent as HttpAgent } from 'http';
|
|
2
|
+
|
|
3
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
4
|
+
import { getProxyForUrl } from 'proxy-from-env';
|
|
5
|
+
import { EnvHttpProxyAgent } from 'undici';
|
|
6
|
+
|
|
7
|
+
import type { Fetch } from './builtin-types';
|
|
8
|
+
|
|
9
|
+
class NodeProxyTransport {
|
|
10
|
+
private envHttpProxyAgent: EnvHttpProxyAgent | undefined;
|
|
11
|
+
private websocketAgents = new Map<string, HttpAgent>();
|
|
12
|
+
|
|
13
|
+
fetch: Fetch = async (input, init) => {
|
|
14
|
+
if (!this.hasProxyEnv()) {
|
|
15
|
+
return fetch(input, init);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (fetch as any)(input, {
|
|
19
|
+
...(init ?? {}),
|
|
20
|
+
dispatcher: this.getEnvHttpProxyAgent(),
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
getWebSocketAgent(url: string): HttpAgent | undefined {
|
|
25
|
+
if (!this.hasProxyEnv()) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const proxyUrl = getProxyForUrl(this.getWebSocketProxyLookupUrl(url));
|
|
30
|
+
if (!proxyUrl) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let agent = this.websocketAgents.get(proxyUrl);
|
|
35
|
+
if (!agent) {
|
|
36
|
+
const createdAgent = new HttpsProxyAgent(proxyUrl);
|
|
37
|
+
this.websocketAgents.set(proxyUrl, createdAgent);
|
|
38
|
+
agent = createdAgent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return agent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private getEnvHttpProxyAgent(): EnvHttpProxyAgent {
|
|
45
|
+
this.envHttpProxyAgent ??= new EnvHttpProxyAgent();
|
|
46
|
+
return this.envHttpProxyAgent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private hasProxyEnv(): boolean {
|
|
50
|
+
if (typeof process === 'undefined' || !process.versions?.node) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const env = process.env;
|
|
55
|
+
return !!(env['http_proxy'] || env['HTTP_PROXY'] || env['https_proxy'] || env['HTTPS_PROXY']);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private getWebSocketProxyLookupUrl(url: string): string {
|
|
59
|
+
const lookupUrl = new URL(url);
|
|
60
|
+
if (lookupUrl.protocol === 'ws:') {
|
|
61
|
+
lookupUrl.protocol = 'http:';
|
|
62
|
+
} else if (lookupUrl.protocol === 'wss:') {
|
|
63
|
+
lookupUrl.protocol = 'https:';
|
|
64
|
+
}
|
|
65
|
+
return lookupUrl.toString();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const nodeProxyTransport = new NodeProxyTransport();
|
package/src/internal/shims.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Fetch } from './builtin-types';
|
|
11
|
+
import { nodeProxyTransport } from './proxy-transport';
|
|
11
12
|
import type { ReadableStream } from './shim-types';
|
|
12
13
|
|
|
13
14
|
export function getDefaultFetch(): Fetch {
|
|
14
15
|
if (typeof fetch !== 'undefined') {
|
|
15
|
-
return fetch
|
|
16
|
+
return nodeProxyTransport.fetch;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
throw new Error(
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export const readEnv = (env: string): string | undefined => {
|
|
11
11
|
if (typeof (globalThis as any).process !== 'undefined') {
|
|
12
|
-
return (globalThis as any).process.env?.[env]?.trim()
|
|
12
|
+
return (globalThis as any).process.env?.[env]?.trim() || undefined;
|
|
13
13
|
}
|
|
14
14
|
if (typeof (globalThis as any).Deno !== 'undefined') {
|
|
15
|
-
return (globalThis as any).Deno.env?.get?.(env)?.trim();
|
|
15
|
+
return (globalThis as any).Deno.env?.get?.(env)?.trim() || undefined;
|
|
16
16
|
}
|
|
17
17
|
return undefined;
|
|
18
18
|
};
|
package/src/ios-client.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { WebSocket, Data } from 'ws';
|
|
2
|
-
import fs from 'fs';
|
|
3
1
|
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
2
|
import crypto from 'crypto';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { WebSocket, Data } from 'ws';
|
|
6
6
|
import { EventEmitter } from 'events';
|
|
7
|
-
import { Readable } from 'stream';
|
|
8
|
-
import { pipeline } from 'stream/promises';
|
|
9
7
|
import { isNonRetryableError } from './tunnel';
|
|
10
8
|
import { type SyncFolderResult, type FolderSyncOptions, syncFolder } from './folder-sync';
|
|
11
9
|
import { createIgnoreFn } from './folder-sync-ignore';
|
|
10
|
+
import { downloadFileToLocalPath } from './internal/download-file';
|
|
11
|
+
import { nodeProxyTransport } from './internal/proxy-transport';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Connection state of the instance client
|
|
@@ -29,33 +29,6 @@ function generateRecordingFilename(): string {
|
|
|
29
29
|
return `ios_video_${formattedDate}_${rand}.mp4`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
async function downloadFileToLocalPath(url: string, token: string, localPath: string): Promise<void> {
|
|
33
|
-
const maxRetries = 3;
|
|
34
|
-
|
|
35
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
36
|
-
const response = await fetch(url, {
|
|
37
|
-
method: 'GET',
|
|
38
|
-
headers: {
|
|
39
|
-
Authorization: `Bearer ${token}`,
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
const errorBody = await response.text();
|
|
44
|
-
const isRetriable = response.status >= 500 && response.status < 600;
|
|
45
|
-
if (isRetriable && attempt < maxRetries) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
throw new Error(`Download failed: ${response.status} ${errorBody}`);
|
|
49
|
-
}
|
|
50
|
-
if (!response.body) {
|
|
51
|
-
throw new Error('Download failed: response body is missing');
|
|
52
|
-
}
|
|
53
|
-
await fs.promises.mkdir(path.dirname(localPath), { recursive: true });
|
|
54
|
-
await pipeline(Readable.fromWeb(response.body as any), fs.createWriteStream(localPath));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
32
|
function buildDownloadUrl(apiUrl: string, filename: string): string {
|
|
60
33
|
return `${apiUrl}/files?name=${encodeURIComponent(filename)}`;
|
|
61
34
|
}
|
|
@@ -849,7 +822,8 @@ export class LogStream extends EventEmitter {
|
|
|
849
822
|
|
|
850
823
|
/** @internal - Establish the dedicated WebSocket connection */
|
|
851
824
|
private _connect(): void {
|
|
852
|
-
|
|
825
|
+
const proxyAgent = nodeProxyTransport.getWebSocketAgent(this.wsUrl);
|
|
826
|
+
this.ws = new WebSocket(this.wsUrl, proxyAgent ? { agent: proxyAgent } : {});
|
|
853
827
|
|
|
854
828
|
this.ws.on('open', () => {
|
|
855
829
|
if (this.stopped) {
|
|
@@ -1121,7 +1095,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1121
1095
|
cleanup();
|
|
1122
1096
|
updateConnectionState('connecting');
|
|
1123
1097
|
|
|
1124
|
-
|
|
1098
|
+
const proxyAgent = nodeProxyTransport.getWebSocketAgent(endpointWebSocketUrl);
|
|
1099
|
+
ws = new WebSocket(endpointWebSocketUrl, proxyAgent ? { agent: proxyAgent } : {});
|
|
1125
1100
|
|
|
1126
1101
|
ws.on('message', (data: Data) => {
|
|
1127
1102
|
let message: ServerResponse;
|
|
@@ -1594,7 +1569,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1594
1569
|
try {
|
|
1595
1570
|
// Node's fetch (undici) supports streaming request bodies but TS DOM types may not include
|
|
1596
1571
|
// `duplex` and may not accept Node ReadStreams as BodyInit in some configs.
|
|
1597
|
-
const response = await fetch(uploadUrl, {
|
|
1572
|
+
const response = await nodeProxyTransport.fetch(uploadUrl, {
|
|
1598
1573
|
method: 'PUT',
|
|
1599
1574
|
headers: {
|
|
1600
1575
|
'Content-Type': 'application/octet-stream',
|
|
@@ -72,8 +72,8 @@ export namespace AndroidInstance {
|
|
|
72
72
|
export interface Spec {
|
|
73
73
|
/**
|
|
74
74
|
* After how many minutes of inactivity should the instance be terminated. Example
|
|
75
|
-
* values 1m, 10m, 3h. Default is 3m. Providing "0"
|
|
76
|
-
*
|
|
75
|
+
* values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
|
|
76
|
+
* inactivity timeout.
|
|
77
77
|
*/
|
|
78
78
|
inactivityTimeout: string;
|
|
79
79
|
|
|
@@ -97,6 +97,8 @@ export namespace AndroidInstance {
|
|
|
97
97
|
|
|
98
98
|
adbWebSocketUrl?: string;
|
|
99
99
|
|
|
100
|
+
apiUrl?: string;
|
|
101
|
+
|
|
100
102
|
endpointWebSocketUrl?: string;
|
|
101
103
|
|
|
102
104
|
errorMessage?: string;
|
|
@@ -162,8 +164,8 @@ export namespace AndroidInstanceCreateParams {
|
|
|
162
164
|
|
|
163
165
|
/**
|
|
164
166
|
* After how many minutes of inactivity should the instance be terminated. Example
|
|
165
|
-
* values 1m, 10m, 3h. Default is 3m. Providing "0"
|
|
166
|
-
*
|
|
167
|
+
* values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
|
|
168
|
+
* inactivity timeout.
|
|
167
169
|
*/
|
|
168
170
|
inactivityTimeout?: string;
|
|
169
171
|
|
|
@@ -3,6 +3,7 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
import { promises as fs } from 'fs';
|
|
4
4
|
|
|
5
5
|
import { RequestOptions } from '../internal/request-options';
|
|
6
|
+
import { nodeProxyTransport } from '../internal/proxy-transport';
|
|
6
7
|
import { Assets as GeneratedAssets } from './assets';
|
|
7
8
|
|
|
8
9
|
export interface AssetGetOrUploadParams {
|
|
@@ -45,7 +46,7 @@ export class Assets extends GeneratedAssets {
|
|
|
45
46
|
md5: creationResponse.md5,
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
|
-
const uploadResponse = await fetch(creationResponse.signedUploadUrl, {
|
|
49
|
+
const uploadResponse = await nodeProxyTransport.fetch(creationResponse.signedUploadUrl, {
|
|
49
50
|
headers: {
|
|
50
51
|
'Content-Length': data.length.toString(),
|
|
51
52
|
'Content-Type': 'application/octet-stream',
|
package/src/resources/index.ts
CHANGED
|
@@ -22,5 +22,12 @@ export {
|
|
|
22
22
|
type IosInstanceListParams,
|
|
23
23
|
type IosInstancesItems,
|
|
24
24
|
} from './ios-instances';
|
|
25
|
+
export {
|
|
26
|
+
XcodeInstances,
|
|
27
|
+
type XcodeInstance,
|
|
28
|
+
type XcodeInstanceCreateParams,
|
|
29
|
+
type XcodeInstanceListParams,
|
|
30
|
+
type XcodeInstancesItems,
|
|
31
|
+
} from './xcode-instances';
|
|
25
32
|
|
|
26
33
|
export { Assets, AssetGetOrUploadParams, AssetGetOrUploadResponse } from './assets-helpers';
|
|
@@ -72,8 +72,8 @@ export namespace IosInstance {
|
|
|
72
72
|
export interface Spec {
|
|
73
73
|
/**
|
|
74
74
|
* After how many minutes of inactivity should the instance be terminated. Example
|
|
75
|
-
* values 1m, 10m, 3h. Default is 3m. Providing "0"
|
|
76
|
-
*
|
|
75
|
+
* values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
|
|
76
|
+
* inactivity timeout.
|
|
77
77
|
*/
|
|
78
78
|
inactivityTimeout: string;
|
|
79
79
|
|
|
@@ -162,8 +162,8 @@ export namespace IosInstanceCreateParams {
|
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* After how many minutes of inactivity should the instance be terminated. Example
|
|
165
|
-
* values 1m, 10m, 3h. Default is 3m. Providing "0"
|
|
166
|
-
*
|
|
165
|
+
* values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
|
|
166
|
+
* inactivity timeout.
|
|
167
167
|
*/
|
|
168
168
|
inactivityTimeout?: string;
|
|
169
169
|
|